mirror of
https://github.com/remnawave/python-sdk.git
synced 2026-05-13 12:16:42 +00:00
feat: Add new endpoints for fetching user IPs and recap statistics
- Implemented `fetch_users_ips` and `get_fetch_users_ips_result` in `IpControlController`. - Added `get_recap` endpoint in `SystemController`. - Introduced new models for user IP fetching and recap statistics in `models/ip_control.py` and `models/system.py`. - Updated existing models and enums to accommodate new features. - Added tests for new endpoints and model validations.
This commit is contained in:
parent
a880da073f
commit
3e6be1f4b4
18 changed files with 818 additions and 35 deletions
|
|
@ -8,6 +8,8 @@ from remnawave.models import (
|
|||
DropConnectionsResponseDto,
|
||||
FetchIpsResponseDto,
|
||||
FetchIpsResultResponseDto,
|
||||
FetchUsersIpsResponseDto,
|
||||
FetchUsersIpsResultResponseDto,
|
||||
)
|
||||
from remnawave.rapid import BaseController, get, post
|
||||
|
||||
|
|
@ -41,6 +43,33 @@ class IpControlController(BaseController):
|
|||
"""
|
||||
...
|
||||
|
||||
@post("/ip-control/fetch-users-ips/{nodeUuid}", response_class=FetchUsersIpsResponseDto)
|
||||
async def fetch_users_ips(
|
||||
self,
|
||||
nodeUuid: Annotated[str, Path(description="UUID of the node")],
|
||||
) -> FetchUsersIpsResponseDto:
|
||||
"""Request IP List for all users on a node.
|
||||
|
||||
Starts a background job that queries the specified node for the IPs
|
||||
of all connected users. The returned ``job_id`` must be passed to
|
||||
:meth:`get_fetch_users_ips_result` to retrieve the actual list once
|
||||
the job is complete.
|
||||
"""
|
||||
...
|
||||
|
||||
@get("/ip-control/fetch-users-ips/result/{jobId}", response_class=FetchUsersIpsResultResponseDto)
|
||||
async def get_fetch_users_ips_result(
|
||||
self,
|
||||
jobId: Annotated[str, Path(description="Job ID returned by fetch_users_ips")],
|
||||
) -> FetchUsersIpsResultResponseDto:
|
||||
"""Get Users IP List Result by Job ID.
|
||||
|
||||
Poll this endpoint after calling :meth:`fetch_users_ips`. When
|
||||
``is_completed`` is ``True`` the ``result`` field contains per-user
|
||||
IP lists.
|
||||
"""
|
||||
...
|
||||
|
||||
@post("/ip-control/drop-connections", response_class=DropConnectionsResponseDto)
|
||||
async def drop_connections(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ from remnawave.models import (
|
|||
EncryptHappCryptoLinkResponseDto,
|
||||
DebugSrrMatcherRequestDto,
|
||||
DebugSrrMatcherResponseDto,
|
||||
GetMetadataResponseDto
|
||||
GetMetadataResponseDto,
|
||||
GetRecapResponseDto,
|
||||
)
|
||||
from remnawave.rapid import BaseController, get, post
|
||||
|
||||
|
|
@ -80,4 +81,11 @@ class SystemController(BaseController):
|
|||
body: Annotated[DebugSrrMatcherRequestDto, PydanticBody()],
|
||||
) -> DebugSrrMatcherResponseDto:
|
||||
"""Test SRR Matcher"""
|
||||
...
|
||||
|
||||
@get("/system/stats/recap", response_class=GetRecapResponseDto)
|
||||
async def get_recap(
|
||||
self,
|
||||
) -> GetRecapResponseDto:
|
||||
"""Get Recap"""
|
||||
...
|
||||
|
|
@ -21,6 +21,12 @@ from remnawave.models import (
|
|||
UpdateUserRequestDto,
|
||||
UpdateUserResponseDto,
|
||||
RevokeUserRequestDto,
|
||||
RevokeUserSubscriptionResponseDto,
|
||||
DisableUserResponseDto,
|
||||
EnableUserResponseDto,
|
||||
ResetUserTrafficResponseDto,
|
||||
ResolveUserRequestBodyDto,
|
||||
ResolveUserResponseDto,
|
||||
)
|
||||
from remnawave.rapid import BaseController, delete, get, patch, post
|
||||
|
||||
|
|
@ -65,36 +71,36 @@ class UsersController(BaseController):
|
|||
"""Delete user"""
|
||||
...
|
||||
|
||||
@post("/users/{uuid}/actions/revoke", response_class=UpdateUserResponseDto)
|
||||
@post("/users/{uuid}/actions/revoke", response_class=RevokeUserSubscriptionResponseDto)
|
||||
async def revoke_user_subscription(
|
||||
self,
|
||||
uuid: Annotated[str, Path(description="UUID of the user")],
|
||||
body: Optional[Annotated[RevokeUserRequestDto, PydanticBody()]] = None,
|
||||
) -> UpdateUserResponseDto:
|
||||
) -> RevokeUserSubscriptionResponseDto:
|
||||
"""Revoke User Subscription"""
|
||||
...
|
||||
|
||||
@post("/users/{uuid}/actions/disable", response_class=UpdateUserResponseDto)
|
||||
@post("/users/{uuid}/actions/disable", response_class=DisableUserResponseDto)
|
||||
async def disable_user(
|
||||
self,
|
||||
uuid: Annotated[str, Path(description="UUID of the user")],
|
||||
) -> UpdateUserResponseDto:
|
||||
) -> DisableUserResponseDto:
|
||||
"""Disable User"""
|
||||
...
|
||||
|
||||
@post("/users/{uuid}/actions/enable", response_class=UpdateUserResponseDto)
|
||||
@post("/users/{uuid}/actions/enable", response_class=EnableUserResponseDto)
|
||||
async def enable_user(
|
||||
self,
|
||||
uuid: Annotated[str, Path(description="UUID of the user")],
|
||||
) -> UpdateUserResponseDto:
|
||||
) -> EnableUserResponseDto:
|
||||
"""Enable User"""
|
||||
...
|
||||
|
||||
@post("/users/{uuid}/actions/reset-traffic", response_class=UpdateUserResponseDto)
|
||||
@post("/users/{uuid}/actions/reset-traffic", response_class=ResetUserTrafficResponseDto)
|
||||
async def reset_user_traffic(
|
||||
self,
|
||||
uuid: Annotated[str, Path(description="UUID of the user")],
|
||||
) -> UpdateUserResponseDto:
|
||||
) -> ResetUserTrafficResponseDto:
|
||||
"""Reset User Traffic"""
|
||||
...
|
||||
|
||||
|
|
@ -186,4 +192,12 @@ class UsersController(BaseController):
|
|||
tag: Annotated[str, Path(description="Tag of the user")],
|
||||
) -> TagUserResponseDto:
|
||||
"""Get Users By Tag"""
|
||||
...
|
||||
|
||||
@post("/users/resolve", response_class=ResolveUserResponseDto)
|
||||
async def resolve_user(
|
||||
self,
|
||||
body: Annotated[ResolveUserRequestBodyDto, PydanticBody()],
|
||||
) -> ResolveUserResponseDto:
|
||||
"""Resolve user by any identifier (uuid, id, shortUuid, username)"""
|
||||
...
|
||||
|
|
@ -6,7 +6,7 @@ from .security_layer import SecurityLayer
|
|||
from .template_type import TemplateType
|
||||
from .users import TrafficLimitStrategy, UserStatus
|
||||
from .webhook import (
|
||||
TCRMEvents, TErrorsEvents, TNodeEvents, TResetPeriods, TServiceEvents, TUserEvents, TUserHwidDevicesEvents, TUsersStatus
|
||||
TCRMEvents, TErrorsEvents, TNodeEvents, TResetPeriods, TServiceEvents, TUserEvents, TUserHwidDevicesEvents, TUsersStatus, TTorrentBlockerEvents
|
||||
)
|
||||
from .auth import OAuth2Provider
|
||||
from .subscriptions_settings import (
|
||||
|
|
@ -41,4 +41,5 @@ __all__ = [
|
|||
"TUserHwidDevicesEvents",
|
||||
"TResetPeriods",
|
||||
"TUsersStatus",
|
||||
"TTorrentBlockerEvents",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ from enum import StrEnum
|
|||
|
||||
class OAuth2Provider(StrEnum):
|
||||
"""OAuth2 Provider enum"""
|
||||
TELEGRAM = "telegram"
|
||||
GITHUB = "github"
|
||||
POCKETID = "pocketid"
|
||||
YANDEX = "yandex"
|
||||
KEYCLOAK = "keycloak"
|
||||
KEYCLOAK = "keycloak"
|
||||
GENERIC = "generic"
|
||||
|
|
@ -7,4 +7,5 @@ class TemplateType(StrEnum):
|
|||
SINGBOX_LEGACY = "SINGBOX_LEGACY"
|
||||
MIHOMO = "MIHOMO"
|
||||
XRAY_JSON = "XRAY_JSON"
|
||||
XRAY_BASE64 = "XRAY_BASE64"
|
||||
CLASH = "CLASH"
|
||||
|
|
|
|||
|
|
@ -13,3 +13,4 @@ class TrafficLimitStrategy(StrEnum):
|
|||
DAY = "DAY"
|
||||
WEEK = "WEEK"
|
||||
MONTH = "MONTH"
|
||||
MONTH_ROLLING = "MONTH_ROLLING"
|
||||
|
|
|
|||
|
|
@ -57,5 +57,9 @@ TUserHwidDevicesEvents = Literal[
|
|||
"user_hwid_devices.deleted",
|
||||
]
|
||||
|
||||
TResetPeriods = Literal["NO_RESET", "DAY", "WEEK", "MONTH"]
|
||||
TTorrentBlockerEvents = Literal[
|
||||
"torrent_blocker.report",
|
||||
]
|
||||
|
||||
TResetPeriods = Literal["NO_RESET", "DAY", "WEEK", "MONTH", "MONTH_ROLLING"]
|
||||
TUsersStatus = Literal["DISABLED", "LIMITED", "EXPIRED", "ACTIVE"]
|
||||
|
|
|
|||
|
|
@ -236,6 +236,7 @@ from .subscriptions_settings import (
|
|||
ResponseRule,
|
||||
ResponseRuleCondition,
|
||||
ResponseRules,
|
||||
ResponseRulesSettings,
|
||||
SubscriptionSettingsResponseDto,
|
||||
SubscriptionType,
|
||||
UpdateSubscriptionSettingsRequestDto,
|
||||
|
|
@ -276,19 +277,24 @@ from .system import (
|
|||
StatusCounts,
|
||||
UsersStatistic,
|
||||
GetNodesMetricsResponseDto,
|
||||
GetX25519KeyPairResponseDto,
|
||||
GetX25519KeyPairResponseDto,
|
||||
X25519KeyPair,
|
||||
DebugSrrMatcherRequestDto,
|
||||
DebugSrrMatcherResponseDto,
|
||||
EncryptHappCryptoLinkRequestDto,
|
||||
EncryptHappCryptoLinkResponseDto,
|
||||
GetMetadataResponseDto
|
||||
GetMetadataResponseDto,
|
||||
GetRecapResponseDto,
|
||||
RecapThisMonth,
|
||||
RecapTotal,
|
||||
)
|
||||
from .users import (
|
||||
# Request DTOs
|
||||
CreateUserRequestDto,
|
||||
UpdateUserRequestDto,
|
||||
RevokeUserRequestDto,
|
||||
ResolveUserRequestBodyDto,
|
||||
ResolveUserResponseDto,
|
||||
|
||||
# Response DTOs - Single User
|
||||
CreateUserResponseDto,
|
||||
|
|
@ -378,7 +384,7 @@ from .subscription_request_history import (
|
|||
SubscriptionRequestHistoryStatsData
|
||||
)
|
||||
from .webhook import (
|
||||
UserEventDto,
|
||||
UserEventDto,
|
||||
UserHwidDeviceEventDto,
|
||||
HwidUserDeviceDto,
|
||||
LastConnectedNodeDto,
|
||||
|
|
@ -394,8 +400,15 @@ from .webhook import (
|
|||
NodeEventDto,
|
||||
CustomErrorEventDto,
|
||||
CrmEventDto,
|
||||
TorrentBlockerEventDto,
|
||||
TorrentBlockerReportDto,
|
||||
WebhookPayloadDto,
|
||||
UserTrafficDto
|
||||
UserTrafficDto,
|
||||
NodeSystemDto,
|
||||
NodeSystemInfoDto,
|
||||
NodeSystemStatsDto,
|
||||
NodeSystemInterfaceDto,
|
||||
NodeVersionsDto,
|
||||
)
|
||||
from .passkeys import (
|
||||
DeletePasskeyRequestDto,
|
||||
|
|
@ -518,6 +531,8 @@ from .ip_control import (
|
|||
# Response DTOs
|
||||
FetchIpsResponseDto,
|
||||
FetchIpsResultResponseDto,
|
||||
FetchUsersIpsResponseDto,
|
||||
FetchUsersIpsResultResponseDto,
|
||||
DropConnectionsResponseDto,
|
||||
# Data models
|
||||
FetchIpsJobData,
|
||||
|
|
@ -525,6 +540,11 @@ from .ip_control import (
|
|||
FetchIpsNodeResult,
|
||||
FetchIpsResult,
|
||||
FetchIpsResultData,
|
||||
FetchUsersIpsJobData,
|
||||
FetchUsersIpsUserIp,
|
||||
FetchUsersIpsUser,
|
||||
FetchUsersIpsResult,
|
||||
FetchUsersIpsResultData,
|
||||
DropConnectionsResponseData,
|
||||
)
|
||||
|
||||
|
|
@ -546,10 +566,6 @@ __all__ = [
|
|||
"VerifyPasskeyAuthenticationRequestDto",
|
||||
"VerifyPasskeyAuthenticationResponseDto",
|
||||
"GetPasskeyAuthenticationOptionsResponseDto",
|
||||
"AuthenticationSettings",
|
||||
"PasskeyAuthenticationSettings",
|
||||
"OAuth2ProvidersSettings",
|
||||
"PasswordAuthenticationSettings",
|
||||
"BrandingSettings",
|
||||
# Nodes models
|
||||
"CreateNodeRequestDto",
|
||||
|
|
@ -639,6 +655,7 @@ __all__ = [
|
|||
"ResponseRule",
|
||||
"ResponseRuleCondition",
|
||||
"ResponseRules",
|
||||
"ResponseRulesSettings",
|
||||
# Subscription template models
|
||||
"GetTemplateResponseDto",
|
||||
"TemplateResponseDto",
|
||||
|
|
@ -675,6 +692,9 @@ __all__ = [
|
|||
"EncryptHappCryptoLinkRequestDto",
|
||||
"EncryptHappCryptoLinkResponseDto",
|
||||
"GetMetadataResponseDto",
|
||||
"GetRecapResponseDto",
|
||||
"RecapThisMonth",
|
||||
"RecapTotal",
|
||||
# XRay config models
|
||||
"ConfigResponseDto", # Legacy alias
|
||||
"GetConfigResponseDto",
|
||||
|
|
@ -741,6 +761,8 @@ __all__ = [
|
|||
"CreateUserRequestDto",
|
||||
"UpdateUserRequestDto",
|
||||
"RevokeUserRequestDto",
|
||||
"ResolveUserRequestBodyDto",
|
||||
"ResolveUserResponseDto",
|
||||
"CreateUserResponseDto",
|
||||
"UpdateUserResponseDto",
|
||||
"GetUserByUuidResponseDto",
|
||||
|
|
@ -907,6 +929,17 @@ __all__ = [
|
|||
# CRM EVENTS
|
||||
"CrmEventDto",
|
||||
|
||||
# TORRENT BLOCKER EVENTS
|
||||
"TorrentBlockerEventDto",
|
||||
"TorrentBlockerReportDto",
|
||||
|
||||
# NODE SYSTEM/VERSIONS
|
||||
"NodeSystemDto",
|
||||
"NodeSystemInfoDto",
|
||||
"NodeSystemStatsDto",
|
||||
"NodeSystemInterfaceDto",
|
||||
"NodeVersionsDto",
|
||||
|
||||
# WEBHOOK PAYLOAD
|
||||
"WebhookPayloadDto",
|
||||
|
||||
|
|
@ -1001,6 +1034,13 @@ __all__ = [
|
|||
"FetchIpsNodeResult",
|
||||
"FetchIpsResult",
|
||||
"FetchIpsResultData",
|
||||
"FetchUsersIpsResponseDto",
|
||||
"FetchUsersIpsResultResponseDto",
|
||||
"FetchUsersIpsJobData",
|
||||
"FetchUsersIpsUserIp",
|
||||
"FetchUsersIpsUser",
|
||||
"FetchUsersIpsResult",
|
||||
"FetchUsersIpsResultData",
|
||||
"DropConnectionsResponseData",
|
||||
|
||||
# Metadata models
|
||||
|
|
|
|||
|
|
@ -122,11 +122,9 @@ class DeleteInfraProviderByUuidResponseDto(BaseModel):
|
|||
# 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")
|
||||
amount: float = Field(ge=0)
|
||||
billed_at: datetime = Field(serialization_alias="billedAt")
|
||||
|
||||
|
||||
class CreateInfraBillingHistoryRecordResponseDto(InfraBillingHistoryDto):
|
||||
|
|
@ -150,7 +148,7 @@ class DeleteInfraBillingHistoryRecordByUuidResponseDto(BaseModel):
|
|||
class CreateInfraBillingNodeRequestDto(BaseModel):
|
||||
node_uuid: UUID = Field(serialization_alias="nodeUuid")
|
||||
provider_uuid: UUID = Field(serialization_alias="providerUuid")
|
||||
next_billing_at: datetime = Field(serialization_alias="nextBillingAt")
|
||||
next_billing_at: Optional[datetime] = Field(None, serialization_alias="nextBillingAt")
|
||||
|
||||
|
||||
# ИСПРАВЛЕНО: API возвращает список всех billing nodes после создания, а не один созданный
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from datetime import datetime
|
||||
from typing import Annotated, List, Literal, Optional, Union
|
||||
from uuid import UUID
|
||||
|
||||
|
|
@ -114,6 +115,55 @@ TargetNodes = Annotated[
|
|||
]
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Fetch Users IPs – step 1: start the job
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class FetchUsersIpsJobData(BaseModel):
|
||||
"""Returned job ID after requesting users IP fetch"""
|
||||
job_id: str = Field(alias="jobId")
|
||||
|
||||
|
||||
class FetchUsersIpsResponseDto(FetchUsersIpsJobData):
|
||||
"""Response for POST /api/ip-control/fetch-users-ips/{nodeUuid}"""
|
||||
pass
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Fetch Users IPs – step 2: poll the job result
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class FetchUsersIpsUserIp(BaseModel):
|
||||
"""IP entry with last seen timestamp"""
|
||||
ip: str
|
||||
last_seen: datetime = Field(alias="lastSeen")
|
||||
|
||||
|
||||
class FetchUsersIpsUser(BaseModel):
|
||||
"""Per-user IP list"""
|
||||
user_id: str = Field(alias="userId")
|
||||
ips: List[FetchUsersIpsUserIp]
|
||||
|
||||
|
||||
class FetchUsersIpsResult(BaseModel):
|
||||
"""Full result payload when the job is completed"""
|
||||
success: bool
|
||||
node_uuid: UUID = Field(alias="nodeUuid")
|
||||
users: List[FetchUsersIpsUser]
|
||||
|
||||
|
||||
class FetchUsersIpsResultData(BaseModel):
|
||||
"""Job state + optional result"""
|
||||
is_completed: bool = Field(alias="isCompleted")
|
||||
is_failed: bool = Field(alias="isFailed")
|
||||
result: Optional[FetchUsersIpsResult] = None
|
||||
|
||||
|
||||
class FetchUsersIpsResultResponseDto(FetchUsersIpsResultData):
|
||||
"""Response for GET /api/ip-control/fetch-users-ips/result/{jobId}"""
|
||||
pass
|
||||
|
||||
|
||||
class DropConnectionsRequestDto(BaseModel):
|
||||
"""Request body for POST /api/ip-control/drop-connections"""
|
||||
drop_by: DropBy = Field(
|
||||
|
|
|
|||
|
|
@ -74,10 +74,20 @@ class ResponseRule(BaseModel):
|
|||
)
|
||||
|
||||
|
||||
class ResponseRulesSettings(BaseModel):
|
||||
"""Settings for response rules"""
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
disable_subscription_access_by_path: Optional[bool] = Field(
|
||||
None, alias="disableSubscriptionAccessByPath"
|
||||
)
|
||||
|
||||
|
||||
class ResponseRules(BaseModel):
|
||||
"""Response rules configuration"""
|
||||
version: ResponseRuleVersion
|
||||
rules: List[ResponseRule]
|
||||
settings: Optional[ResponseRulesSettings] = None
|
||||
|
||||
|
||||
class CustomRemarksDto(BaseModel):
|
||||
|
|
|
|||
|
|
@ -213,6 +213,27 @@ class DebugSrrMatcherData(BaseModel):
|
|||
class DebugSrrMatcherResponseDto(DebugSrrMatcherData):
|
||||
pass
|
||||
|
||||
class RecapThisMonth(BaseModel):
|
||||
users: float
|
||||
traffic: str
|
||||
|
||||
|
||||
class RecapTotal(BaseModel):
|
||||
users: float
|
||||
nodes: float
|
||||
traffic: str
|
||||
nodes_ram: str = Field(alias="nodesRam")
|
||||
nodes_cpu_cores: float = Field(alias="nodesCpuCores")
|
||||
distinct_countries: float = Field(alias="distinctCountries")
|
||||
|
||||
|
||||
class GetRecapResponseDto(BaseModel):
|
||||
this_month: RecapThisMonth = Field(alias="thisMonth")
|
||||
total: RecapTotal
|
||||
version: str
|
||||
init_date: datetime.datetime = Field(alias="initDate")
|
||||
|
||||
|
||||
class BuildInfo(BaseModel):
|
||||
"""Build information"""
|
||||
time: str
|
||||
|
|
|
|||
|
|
@ -203,6 +203,22 @@ class RevokeUserRequestDto(BaseModel):
|
|||
description="Optional. If true, only passwords will be revoked without changing the short UUID.",
|
||||
)
|
||||
|
||||
class ResolveUserRequestBodyDto(BaseModel):
|
||||
"""Request DTO for resolving a user by any identifier"""
|
||||
uuid: Optional[UUID] = None
|
||||
id: Optional[int] = None
|
||||
short_uuid: Optional[str] = Field(None, serialization_alias="shortUuid")
|
||||
username: Optional[str] = None
|
||||
|
||||
|
||||
class ResolveUserResponseDto(BaseModel):
|
||||
"""Response DTO for resolved user"""
|
||||
uuid: UUID
|
||||
username: str
|
||||
id: int
|
||||
short_uuid: str = Field(alias="shortUuid")
|
||||
|
||||
|
||||
class SubscriptionRequestRecord(BaseModel):
|
||||
"""Subscription request history record"""
|
||||
id: int
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from uuid import UUID
|
|||
from pydantic import BaseModel, Field
|
||||
from pydantic.alias_generators import to_camel
|
||||
from remnawave.enums import (
|
||||
TUsersStatus, TUserEvents, TUserHwidDevicesEvents, TServiceEvents, TNodeEvents, TErrorsEvents, TCRMEvents, TResetPeriods
|
||||
TUsersStatus, TUserEvents, TUserHwidDevicesEvents, TServiceEvents, TNodeEvents, TErrorsEvents, TCRMEvents, TTorrentBlockerEvents, TResetPeriods
|
||||
)
|
||||
# ---------------- USER ---------------- #
|
||||
|
||||
|
|
@ -223,6 +223,55 @@ class WebhookNodeConfigProfileDto(BaseModel):
|
|||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class NodeSystemInfoDto(BaseModel):
|
||||
arch: str
|
||||
cpus: int
|
||||
cpu_model: str
|
||||
memory_total: float
|
||||
hostname: str
|
||||
platform: str
|
||||
release: str
|
||||
type: str
|
||||
version: str
|
||||
network_interfaces: List[str]
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class NodeSystemInterfaceDto(BaseModel):
|
||||
interface: str
|
||||
rx_bytes_per_sec: float
|
||||
tx_bytes_per_sec: float
|
||||
rx_total: float
|
||||
tx_total: float
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class NodeSystemStatsDto(BaseModel):
|
||||
memory_free: float
|
||||
memory_used: float
|
||||
uptime: float
|
||||
load_avg: List[float]
|
||||
interface: Optional[NodeSystemInterfaceDto] = None
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class NodeSystemDto(BaseModel):
|
||||
info: NodeSystemInfoDto
|
||||
stats: NodeSystemStatsDto
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class NodeVersionsDto(BaseModel):
|
||||
xray: str
|
||||
node: str
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class NodeDto(BaseModel):
|
||||
uuid: UUID
|
||||
name: str
|
||||
|
|
@ -234,11 +283,8 @@ class NodeDto(BaseModel):
|
|||
last_status_change: Optional[datetime] = None
|
||||
last_status_message: Optional[str] = None
|
||||
|
||||
xray_version: Optional[str] = None
|
||||
node_version: Optional[str] = None
|
||||
xray_uptime: str
|
||||
|
||||
users_online: Optional[int] = None
|
||||
xray_uptime: float = 0
|
||||
users_online: Optional[float] = None
|
||||
|
||||
is_traffic_tracking_active: bool
|
||||
traffic_reset_day: Optional[int] = None
|
||||
|
|
@ -252,10 +298,6 @@ class NodeDto(BaseModel):
|
|||
|
||||
tags: List[str] = Field(default_factory=list)
|
||||
|
||||
cpu_count: Optional[int] = None
|
||||
cpu_model: Optional[str] = None
|
||||
total_ram: Optional[str] = None
|
||||
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
|
@ -264,6 +306,10 @@ class NodeDto(BaseModel):
|
|||
provider_uuid: Optional[UUID] = None
|
||||
provider: Optional[InfraProviderDto] = None
|
||||
|
||||
active_plugin_uuid: Optional[UUID] = None
|
||||
system: Optional[NodeSystemDto] = None
|
||||
versions: Optional[NodeVersionsDto] = None
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
# Backward-compat shims for code that used the flat fields directly
|
||||
|
|
@ -318,6 +364,21 @@ class CrmEventDto(BaseModel):
|
|||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
# ---------------- TORRENT BLOCKER EVENTS ---------------- #
|
||||
|
||||
class TorrentBlockerReportDto(BaseModel):
|
||||
node: NodeDto
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class TorrentBlockerEventDto(BaseModel):
|
||||
event_name: TTorrentBlockerEvents
|
||||
data: TorrentBlockerReportDto
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
# ---------------- WEBHOOK PAYLOAD ---------------- #
|
||||
|
||||
class WebhookPayloadDto(BaseModel):
|
||||
|
|
|
|||
119
tests/test_controllers_completeness.py
Normal file
119
tests/test_controllers_completeness.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
"""Tests that all required endpoints exist in controllers."""
|
||||
import pytest
|
||||
import inspect
|
||||
|
||||
from remnawave.controllers.users import UsersController
|
||||
from remnawave.controllers.system import SystemController
|
||||
from remnawave.controllers.ip_control import IpControlController
|
||||
|
||||
|
||||
class TestUsersControllerEndpoints:
|
||||
def test_has_resolve_user(self):
|
||||
assert hasattr(UsersController, "resolve_user")
|
||||
assert callable(getattr(UsersController, "resolve_user"))
|
||||
|
||||
def test_has_revoke_user_subscription(self):
|
||||
assert hasattr(UsersController, "revoke_user_subscription")
|
||||
|
||||
def test_has_disable_user(self):
|
||||
assert hasattr(UsersController, "disable_user")
|
||||
|
||||
def test_has_enable_user(self):
|
||||
assert hasattr(UsersController, "enable_user")
|
||||
|
||||
def test_has_reset_user_traffic(self):
|
||||
assert hasattr(UsersController, "reset_user_traffic")
|
||||
|
||||
def test_has_create_user(self):
|
||||
assert hasattr(UsersController, "create_user")
|
||||
|
||||
def test_has_update_user(self):
|
||||
assert hasattr(UsersController, "update_user")
|
||||
|
||||
def test_has_delete_user(self):
|
||||
assert hasattr(UsersController, "delete_user")
|
||||
|
||||
def test_has_get_all_users(self):
|
||||
assert hasattr(UsersController, "get_all_users")
|
||||
|
||||
def test_has_get_user_by_uuid(self):
|
||||
assert hasattr(UsersController, "get_user_by_uuid")
|
||||
|
||||
def test_has_get_user_by_short_uuid(self):
|
||||
assert hasattr(UsersController, "get_user_by_short_uuid")
|
||||
|
||||
def test_has_get_user_by_username(self):
|
||||
assert hasattr(UsersController, "get_user_by_username")
|
||||
|
||||
def test_has_get_user_by_id(self):
|
||||
assert hasattr(UsersController, "get_user_by_id")
|
||||
|
||||
def test_has_get_users_by_telegram_id(self):
|
||||
assert hasattr(UsersController, "get_users_by_telegram_id")
|
||||
|
||||
def test_has_get_users_by_email(self):
|
||||
assert hasattr(UsersController, "get_users_by_email")
|
||||
|
||||
def test_has_get_users_by_tag(self):
|
||||
assert hasattr(UsersController, "get_users_by_tag")
|
||||
|
||||
def test_has_get_all_tags(self):
|
||||
assert hasattr(UsersController, "get_all_tags")
|
||||
|
||||
def test_has_get_user_accessible_nodes(self):
|
||||
assert hasattr(UsersController, "get_user_accessible_nodes")
|
||||
|
||||
def test_has_get_user_subscription_request_history(self):
|
||||
assert hasattr(UsersController, "get_user_subscription_request_history")
|
||||
|
||||
|
||||
class TestSystemControllerEndpoints:
|
||||
def test_has_get_recap(self):
|
||||
assert hasattr(SystemController, "get_recap")
|
||||
assert callable(getattr(SystemController, "get_recap"))
|
||||
|
||||
def test_has_get_metadata(self):
|
||||
assert hasattr(SystemController, "get_metadata")
|
||||
|
||||
def test_has_get_stats(self):
|
||||
assert hasattr(SystemController, "get_stats")
|
||||
|
||||
def test_has_get_bandwidth_stats(self):
|
||||
assert hasattr(SystemController, "get_bandwidth_stats")
|
||||
|
||||
def test_has_get_nodes_statistics(self):
|
||||
assert hasattr(SystemController, "get_nodes_statistics")
|
||||
|
||||
def test_has_get_health(self):
|
||||
assert hasattr(SystemController, "get_health")
|
||||
|
||||
def test_has_get_nodes_metrics(self):
|
||||
assert hasattr(SystemController, "get_nodes_metrics")
|
||||
|
||||
def test_has_get_x25519_key_pair(self):
|
||||
assert hasattr(SystemController, "get_x25519_key_pair")
|
||||
|
||||
def test_has_encrypt_happ_crypto_link(self):
|
||||
assert hasattr(SystemController, "encrypt_happ_crypto_link")
|
||||
|
||||
def test_has_debug_srr_matcher(self):
|
||||
assert hasattr(SystemController, "debug_srr_matcher")
|
||||
|
||||
|
||||
class TestIpControlControllerEndpoints:
|
||||
def test_has_fetch_user_ips(self):
|
||||
assert hasattr(IpControlController, "fetch_user_ips")
|
||||
|
||||
def test_has_get_fetch_ips_result(self):
|
||||
assert hasattr(IpControlController, "get_fetch_ips_result")
|
||||
|
||||
def test_has_fetch_users_ips(self):
|
||||
assert hasattr(IpControlController, "fetch_users_ips")
|
||||
assert callable(getattr(IpControlController, "fetch_users_ips"))
|
||||
|
||||
def test_has_get_fetch_users_ips_result(self):
|
||||
assert hasattr(IpControlController, "get_fetch_users_ips_result")
|
||||
assert callable(getattr(IpControlController, "get_fetch_users_ips_result"))
|
||||
|
||||
def test_has_drop_connections(self):
|
||||
assert hasattr(IpControlController, "drop_connections")
|
||||
131
tests/test_enums.py
Normal file
131
tests/test_enums.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
"""Tests for enum completeness against the OpenAPI spec."""
|
||||
import pytest
|
||||
|
||||
from remnawave.enums import (
|
||||
ALPN,
|
||||
ClientType,
|
||||
Fingerprint,
|
||||
OAuth2Provider,
|
||||
SecurityLayer,
|
||||
TemplateType,
|
||||
TrafficLimitStrategy,
|
||||
UserStatus,
|
||||
ResponseRuleConditionOperator,
|
||||
ResponseRuleOperator,
|
||||
ResponseRuleVersion,
|
||||
ResponseType,
|
||||
SubscriptionType,
|
||||
TTorrentBlockerEvents,
|
||||
)
|
||||
|
||||
|
||||
class TestOAuth2Provider:
|
||||
def test_has_telegram(self):
|
||||
assert OAuth2Provider.TELEGRAM == "telegram"
|
||||
|
||||
def test_has_generic(self):
|
||||
assert OAuth2Provider.GENERIC == "generic"
|
||||
|
||||
def test_has_github(self):
|
||||
assert OAuth2Provider.GITHUB == "github"
|
||||
|
||||
def test_has_pocketid(self):
|
||||
assert OAuth2Provider.POCKETID == "pocketid"
|
||||
|
||||
def test_has_yandex(self):
|
||||
assert OAuth2Provider.YANDEX == "yandex"
|
||||
|
||||
def test_has_keycloak(self):
|
||||
assert OAuth2Provider.KEYCLOAK == "keycloak"
|
||||
|
||||
def test_all_values(self):
|
||||
expected = {"telegram", "github", "pocketid", "yandex", "keycloak", "generic"}
|
||||
actual = {v.value for v in OAuth2Provider}
|
||||
assert actual == expected
|
||||
|
||||
|
||||
class TestTemplateType:
|
||||
def test_has_xray_base64(self):
|
||||
assert TemplateType.XRAY_BASE64 == "XRAY_BASE64"
|
||||
|
||||
def test_has_xray_json(self):
|
||||
assert TemplateType.XRAY_JSON == "XRAY_JSON"
|
||||
|
||||
def test_all_api_values(self):
|
||||
api_values = {"XRAY_JSON", "XRAY_BASE64", "MIHOMO", "STASH", "CLASH", "SINGBOX"}
|
||||
actual = {v.value for v in TemplateType}
|
||||
assert api_values.issubset(actual)
|
||||
|
||||
|
||||
class TestTrafficLimitStrategy:
|
||||
def test_has_month_rolling(self):
|
||||
assert TrafficLimitStrategy.MONTH_ROLLING == "MONTH_ROLLING"
|
||||
|
||||
def test_all_api_values(self):
|
||||
api_values = {"NO_RESET", "DAY", "WEEK", "MONTH", "MONTH_ROLLING"}
|
||||
actual = {v.value for v in TrafficLimitStrategy}
|
||||
assert api_values == actual
|
||||
|
||||
|
||||
class TestUserStatus:
|
||||
def test_all_values(self):
|
||||
expected = {"ACTIVE", "DISABLED", "LIMITED", "EXPIRED"}
|
||||
actual = {v.value for v in UserStatus}
|
||||
assert actual == expected
|
||||
|
||||
|
||||
class TestClientType:
|
||||
def test_api_values_present(self):
|
||||
api_values = {"stash", "singbox", "mihomo", "json", "v2ray-json", "clash"}
|
||||
actual = {v.value for v in ClientType}
|
||||
assert api_values.issubset(actual)
|
||||
|
||||
|
||||
class TestSecurityLayer:
|
||||
def test_all_values(self):
|
||||
expected = {"DEFAULT", "TLS", "NONE"}
|
||||
actual = {v.value for v in SecurityLayer}
|
||||
assert actual == expected
|
||||
|
||||
|
||||
class TestFingerprint:
|
||||
def test_all_api_values(self):
|
||||
api_values = {"chrome", "firefox", "safari", "ios", "android", "edge", "qq", "random", "randomized"}
|
||||
actual = {v.value for v in Fingerprint}
|
||||
assert api_values == actual
|
||||
|
||||
|
||||
class TestALPN:
|
||||
def test_has_h3(self):
|
||||
assert "h3" in {v.value for v in ALPN}
|
||||
|
||||
def test_has_h2(self):
|
||||
assert "h2" in {v.value for v in ALPN}
|
||||
|
||||
|
||||
class TestResponseRuleOperator:
|
||||
def test_all_values(self):
|
||||
expected = {"AND", "OR"}
|
||||
actual = {v.value for v in ResponseRuleOperator}
|
||||
assert actual == expected
|
||||
|
||||
|
||||
class TestResponseRuleConditionOperator:
|
||||
def test_all_values(self):
|
||||
expected = {
|
||||
"EQUALS", "NOT_EQUALS", "CONTAINS", "NOT_CONTAINS",
|
||||
"STARTS_WITH", "NOT_STARTS_WITH", "ENDS_WITH", "NOT_ENDS_WITH",
|
||||
"REGEX", "NOT_REGEX",
|
||||
}
|
||||
actual = {v.value for v in ResponseRuleConditionOperator}
|
||||
assert actual == expected
|
||||
|
||||
|
||||
class TestResponseType:
|
||||
def test_all_values(self):
|
||||
expected = {
|
||||
"XRAY_JSON", "XRAY_BASE64", "MIHOMO", "STASH", "CLASH", "SINGBOX",
|
||||
"BROWSER", "BLOCK", "STATUS_CODE_404", "STATUS_CODE_451", "SOCKET_DROP",
|
||||
}
|
||||
actual = {v.value for v in ResponseType}
|
||||
assert actual == expected
|
||||
277
tests/test_models_validation.py
Normal file
277
tests/test_models_validation.py
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
"""Tests for model field validation and serialization."""
|
||||
import pytest
|
||||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
|
||||
from remnawave.models import (
|
||||
# Users
|
||||
ResolveUserRequestBodyDto,
|
||||
ResolveUserResponseDto,
|
||||
RevokeUserRequestDto,
|
||||
# System
|
||||
GetRecapResponseDto,
|
||||
RecapThisMonth,
|
||||
RecapTotal,
|
||||
# IP Control
|
||||
FetchUsersIpsResponseDto,
|
||||
FetchUsersIpsResultResponseDto,
|
||||
FetchUsersIpsUserIp,
|
||||
FetchUsersIpsUser,
|
||||
FetchUsersIpsResult,
|
||||
DropConnectionsRequestDto,
|
||||
DropByUserUuids,
|
||||
DropByIpAddresses,
|
||||
TargetAllNodes,
|
||||
TargetSpecificNodes,
|
||||
# Infra Billing
|
||||
CreateInfraBillingHistoryRecordRequestDto,
|
||||
CreateInfraBillingNodeRequestDto,
|
||||
# Subscription Settings
|
||||
ResponseRules,
|
||||
ResponseRulesSettings,
|
||||
# Webhook
|
||||
NodeSystemDto,
|
||||
NodeSystemInfoDto,
|
||||
NodeSystemStatsDto,
|
||||
NodeVersionsDto,
|
||||
)
|
||||
from remnawave.enums import ResponseRuleVersion
|
||||
|
||||
|
||||
class TestResolveUserRequestBodyDto:
|
||||
def test_create_with_uuid(self):
|
||||
uid = uuid4()
|
||||
dto = ResolveUserRequestBodyDto(uuid=uid)
|
||||
assert dto.uuid == uid
|
||||
assert dto.id is None
|
||||
assert dto.username is None
|
||||
|
||||
def test_create_with_username(self):
|
||||
dto = ResolveUserRequestBodyDto(username="testuser")
|
||||
assert dto.username == "testuser"
|
||||
assert dto.uuid is None
|
||||
|
||||
def test_create_with_short_uuid(self):
|
||||
dto = ResolveUserRequestBodyDto(short_uuid="abc123")
|
||||
assert dto.short_uuid == "abc123"
|
||||
|
||||
def test_serialization_alias(self):
|
||||
dto = ResolveUserRequestBodyDto(short_uuid="abc123")
|
||||
data = dto.model_dump(by_alias=True)
|
||||
assert "shortUuid" in data
|
||||
|
||||
def test_create_with_id(self):
|
||||
dto = ResolveUserRequestBodyDto(id=42)
|
||||
assert dto.id == 42
|
||||
|
||||
|
||||
class TestResolveUserResponseDto:
|
||||
def test_from_api_response(self):
|
||||
uid = uuid4()
|
||||
dto = ResolveUserResponseDto(
|
||||
uuid=uid,
|
||||
username="testuser",
|
||||
id=1,
|
||||
shortUuid="abc123",
|
||||
)
|
||||
assert dto.uuid == uid
|
||||
assert dto.username == "testuser"
|
||||
assert dto.id == 1
|
||||
assert dto.short_uuid == "abc123"
|
||||
|
||||
|
||||
class TestGetRecapResponseDto:
|
||||
def test_from_api_response(self):
|
||||
dto = GetRecapResponseDto(
|
||||
thisMonth={"users": 10, "traffic": "1.5 GB"},
|
||||
total={
|
||||
"users": 100,
|
||||
"nodes": 5,
|
||||
"traffic": "500 GB",
|
||||
"nodesRam": "32 GB",
|
||||
"nodesCpuCores": 16,
|
||||
"distinctCountries": 3,
|
||||
},
|
||||
version="1.11.0",
|
||||
initDate="2025-01-01T00:00:00Z",
|
||||
)
|
||||
assert dto.this_month.users == 10
|
||||
assert dto.this_month.traffic == "1.5 GB"
|
||||
assert dto.total.nodes == 5
|
||||
assert dto.total.nodes_ram == "32 GB"
|
||||
assert dto.total.nodes_cpu_cores == 16
|
||||
assert dto.total.distinct_countries == 3
|
||||
assert dto.version == "1.11.0"
|
||||
assert isinstance(dto.init_date, datetime)
|
||||
|
||||
|
||||
class TestFetchUsersIpsModels:
|
||||
def test_response_dto(self):
|
||||
dto = FetchUsersIpsResponseDto(jobId="job-123")
|
||||
assert dto.job_id == "job-123"
|
||||
|
||||
def test_result_not_completed(self):
|
||||
dto = FetchUsersIpsResultResponseDto(
|
||||
isCompleted=False,
|
||||
isFailed=False,
|
||||
result=None,
|
||||
)
|
||||
assert dto.is_completed is False
|
||||
assert dto.is_failed is False
|
||||
assert dto.result is None
|
||||
|
||||
def test_result_completed(self):
|
||||
uid = uuid4()
|
||||
dto = FetchUsersIpsResultResponseDto(
|
||||
isCompleted=True,
|
||||
isFailed=False,
|
||||
result={
|
||||
"success": True,
|
||||
"nodeUuid": str(uid),
|
||||
"users": [
|
||||
{
|
||||
"userId": "user-1",
|
||||
"ips": [
|
||||
{"ip": "1.2.3.4", "lastSeen": "2025-01-01T00:00:00Z"},
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
assert dto.is_completed is True
|
||||
assert dto.result.success is True
|
||||
assert dto.result.node_uuid == uid
|
||||
assert len(dto.result.users) == 1
|
||||
assert dto.result.users[0].user_id == "user-1"
|
||||
assert dto.result.users[0].ips[0].ip == "1.2.3.4"
|
||||
|
||||
|
||||
class TestCreateInfraBillingHistoryRecordRequestDto:
|
||||
def test_fields_match_spec(self):
|
||||
uid = uuid4()
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
dto = CreateInfraBillingHistoryRecordRequestDto(
|
||||
provider_uuid=uid,
|
||||
amount=29.99,
|
||||
billed_at=now,
|
||||
)
|
||||
assert dto.provider_uuid == uid
|
||||
assert dto.amount == 29.99
|
||||
assert dto.billed_at == now
|
||||
|
||||
def test_serialization(self):
|
||||
uid = uuid4()
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
dto = CreateInfraBillingHistoryRecordRequestDto(
|
||||
provider_uuid=uid,
|
||||
amount=10.0,
|
||||
billed_at=now,
|
||||
)
|
||||
data = dto.model_dump(by_alias=True)
|
||||
assert "providerUuid" in data
|
||||
assert "billedAt" in data
|
||||
assert "amount" in data
|
||||
|
||||
def test_no_old_fields(self):
|
||||
"""Ensure removed fields don't exist."""
|
||||
assert not hasattr(CreateInfraBillingHistoryRecordRequestDto, "node_uuid")
|
||||
assert not hasattr(CreateInfraBillingHistoryRecordRequestDto, "payment_date")
|
||||
assert not hasattr(CreateInfraBillingHistoryRecordRequestDto, "description")
|
||||
|
||||
|
||||
class TestCreateInfraBillingNodeRequestDto:
|
||||
def test_next_billing_at_optional(self):
|
||||
dto = CreateInfraBillingNodeRequestDto(
|
||||
node_uuid=uuid4(),
|
||||
provider_uuid=uuid4(),
|
||||
)
|
||||
assert dto.next_billing_at is None
|
||||
|
||||
def test_next_billing_at_provided(self):
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
dto = CreateInfraBillingNodeRequestDto(
|
||||
node_uuid=uuid4(),
|
||||
provider_uuid=uuid4(),
|
||||
next_billing_at=now,
|
||||
)
|
||||
assert dto.next_billing_at == now
|
||||
|
||||
|
||||
class TestResponseRulesSettings:
|
||||
def test_settings_field_exists(self):
|
||||
rules = ResponseRules(
|
||||
version=ResponseRuleVersion.V1,
|
||||
rules=[],
|
||||
settings=ResponseRulesSettings(
|
||||
disable_subscription_access_by_path=True,
|
||||
),
|
||||
)
|
||||
assert rules.settings is not None
|
||||
assert rules.settings.disable_subscription_access_by_path is True
|
||||
|
||||
def test_settings_optional(self):
|
||||
rules = ResponseRules(
|
||||
version=ResponseRuleVersion.V1,
|
||||
rules=[],
|
||||
)
|
||||
assert rules.settings is None
|
||||
|
||||
def test_settings_deserialization(self):
|
||||
rules = ResponseRules.model_validate({
|
||||
"version": "1",
|
||||
"rules": [],
|
||||
"settings": {"disableSubscriptionAccessByPath": False},
|
||||
})
|
||||
assert rules.settings.disable_subscription_access_by_path is False
|
||||
|
||||
|
||||
class TestWebhookNodeDto:
|
||||
def test_system_field(self):
|
||||
system = NodeSystemDto.model_validate({
|
||||
"info": {
|
||||
"arch": "x64",
|
||||
"cpus": 4,
|
||||
"cpuModel": "Intel Core i7",
|
||||
"memoryTotal": 16384,
|
||||
"hostname": "node-1",
|
||||
"platform": "linux",
|
||||
"release": "5.15.0",
|
||||
"type": "Linux",
|
||||
"version": "#1 SMP",
|
||||
"networkInterfaces": ["eth0", "lo"],
|
||||
},
|
||||
"stats": {
|
||||
"memoryFree": 8192,
|
||||
"memoryUsed": 8192,
|
||||
"uptime": 3600,
|
||||
"loadAvg": [0.5, 0.3, 0.1],
|
||||
"interface": None,
|
||||
},
|
||||
})
|
||||
assert system.info.arch == "x64"
|
||||
assert system.info.cpus == 4
|
||||
assert system.info.cpu_model == "Intel Core i7"
|
||||
assert system.stats.memory_free == 8192
|
||||
assert system.stats.uptime == 3600
|
||||
|
||||
def test_versions_field(self):
|
||||
versions = NodeVersionsDto.model_validate({
|
||||
"xray": "1.8.6",
|
||||
"node": "0.5.0",
|
||||
})
|
||||
assert versions.xray == "1.8.6"
|
||||
assert versions.node == "0.5.0"
|
||||
|
||||
def test_node_dto_has_new_fields(self):
|
||||
from remnawave.models.webhook import NodeDto
|
||||
|
||||
fields = NodeDto.model_fields
|
||||
assert "active_plugin_uuid" in fields
|
||||
assert "system" in fields
|
||||
assert "versions" in fields
|
||||
|
||||
def test_node_dto_xray_uptime_is_float(self):
|
||||
from remnawave.models.webhook import NodeDto
|
||||
|
||||
field = NodeDto.model_fields["xray_uptime"]
|
||||
assert field.annotation == float or field.annotation is float
|
||||
Loading…
Add table
Add a link
Reference in a new issue