Files
RemnaWaveBOT/tests/test_remnawave_v3.py
T
2026-10-28 13:01:59 +03:00

197 lines
8.5 KiB
Python

import json
import httpx
import pytest
from app.db.models import RemnawaveUser
from app.services.remnawave_client import RemnawaveApiClient, RemnawaveApiError
from tests.helpers import make_remote_user
LEGACY_UUID = 'a1111111-1111-4111-8111-111111111111'
def user_payload(user_id=42):
payload = make_remote_user(
user_uuid=LEGACY_UUID, user_id=user_id, short_uuid=f'short{user_id}',
username=f'user{user_id}', telegram_id=777,
).model_dump(by_alias=True, mode='json')
payload.pop('uuid')
return payload
def client_for(handler, legacy=None):
http = httpx.AsyncClient(base_url='https://panel.test/api/', transport=httpx.MockTransport(handler))
api = RemnawaveApiClient(base_url='https://panel.test', api_token='', client=http,
api_version=3, legacy_user_ids=legacy or {42: LEGACY_UUID})
return api, http
async def test_telegram_search_uses_filtered_stream_and_all_pages():
requests = []
def handler(request):
requests.append(request)
assert request.url.path == '/api/users/stream'
assert request.url.params['telegramId'] == '777'
next_page = 'cursor' in request.url.params
return httpx.Response(200, json={'response': {
'users': [user_payload(43 if next_page else 42)],
'hasMore': not next_page, 'nextCursor': None if next_page else '42',
}})
api, http = client_for(handler)
async with http:
users = await api.get_users_by_telegram_id(777)
assert [u.id for u in users] == [42, 43]
assert str(users[0].uuid) == LEGACY_UUID
assert requests[1].url.params['cursor'] == '42'
async def test_renewal_preserves_local_identity_and_sends_only_numeric_identifier():
def handler(request):
if request.method == 'PATCH':
assert json.loads(request.content) == {'id': 42, 'expireAt': '2027-01-01T00:00:00Z'}
else:
assert request.url.path == '/api/users/42'
return httpx.Response(200, json={'response': user_payload()})
api, http = client_for(handler)
async with http:
user = await api.update_user({'uuid': LEGACY_UUID, 'username': 'user42',
'expireAt': '2027-01-01T00:00:00Z'})
assert str(user.uuid) == LEGACY_UUID
assert user.id == 42
async def test_history_and_hwid_use_ids_and_preserve_local_foreign_keys():
timestamp = '2026-10-19T00:00:00Z'
def handler(request):
path = request.url.path
if path.endswith('subscription-request-history'):
assert '/users/42/' in path
data = {'total': 1, 'records': [{'id': 1, 'userId': 42, 'requestAt': timestamp}]}
elif request.method == 'POST':
body = json.loads(request.content)
assert body['userId'] == 42 and 'userUuid' not in body
data = {'total': 0, 'devices': []}
else:
assert path == '/api/hwid/devices/42'
data = {'total': 1, 'devices': [{'hwid': 'test-device-001', 'userId': 42,
'createdAt': timestamp, 'updatedAt': timestamp}]}
return httpx.Response(200, json={'response': data})
api, http = client_for(handler)
async with http:
history = await api.get_user_subscription_history(LEGACY_UUID)
devices = await api.get_user_hwid_devices(LEGACY_UUID)
await api.delete_user_hwid_device(user_uuid=LEGACY_UUID, hwid='test-device-001')
await api.delete_all_user_hwid_devices(LEGACY_UUID)
assert str(history.records[0].user_uuid) == LEGACY_UUID
assert str(devices.devices[0].user_uuid) == LEGACY_UUID
async def test_uuid_resolution_uses_numeric_id():
def handler(request):
assert json.loads(request.content) == {'id': 42}
return httpx.Response(200, json={'response': {'id': 42, 'username': 'user42', 'shortUuid': 'short42'}})
api, http = client_for(handler)
async with http:
resolved = await api.resolve_user(LEGACY_UUID)
assert str(resolved.uuid) == LEGACY_UUID
async def test_new_user_alias_survives_restart_before_local_cache_sync():
def handler(request):
if request.url.path.endswith('/stream'):
data = {'users': [user_payload(99)], 'hasMore': False, 'nextCursor': None}
else:
assert request.url.path in ('/api/users', '/api/users/99')
data = user_payload(99)
return httpx.Response(200, json={'response': data})
api, http = client_for(handler)
async with http:
first = await api.create_user({'username': 'user99'})
restarted, http2 = client_for(handler)
async with http2:
restored = await restarted.get_user_by_uuid(str(first.uuid))
assert restored.uuid == first.uuid
assert restored.id == 99
async def test_missing_identity_never_updates_an_unrelated_user():
def handler(request):
assert request.method == 'GET'
return httpx.Response(200, json={'response': {'users': [], 'hasMore': False, 'nextCursor': None}})
api, http = client_for(handler)
async with http:
with pytest.raises(RemnawaveApiError) as error:
await api.update_user({'uuid': 'b2222222-2222-4222-8222-222222222222', 'status': 'DISABLED'})
assert error.value.status_code == 404
async def test_stream_repeated_cursor_fails_instead_of_looping():
api, http = client_for(lambda request: httpx.Response(200, json={'response': {
'users': [], 'hasMore': True, 'nextCursor': '42'}}))
async with http:
with pytest.raises(RemnawaveApiError, match='cursor'):
await api.get_users_by_telegram_id(777)
async def test_v3_client_full_sync_preserves_existing_database_identity(session_factory):
from app.config import Settings
from app.services.sync_service import SyncService
from sqlalchemy import select
payload = user_payload()
def handler(request):
if request.url.path.endswith('/stream'):
result = {'users': [payload], 'hasMore': False, 'nextCursor': None}
else:
result = {'users': [payload], 'total': 1}
return httpx.Response(200, json={'response': result})
api, http = client_for(handler)
service = SyncService(settings=Settings.model_construct(bot_admin_ids_raw='', sync_subscription_history=False),
session_factory=session_factory, remnawave_client=api)
async with http:
initial = await service.sync_users_for_telegram(telegram_id=777, username='owner',
first_name='Owner', last_name=None, language_code='ru')
original_id = initial[0].record.id
assert initial[0].record.rw_uuid == LEGACY_UUID
assert await service.sync_all_users(batch_size=100) == 1
refreshed = await service.refresh_cached_users_for_telegram(telegram_id=777)
assert refreshed[0].record.id == original_id
assert refreshed[0].record.owner_telegram_user_id == initial[0].record.owner_telegram_user_id
async with session_factory() as session:
rows = list((await session.scalars(select(RemnawaveUser))).all())
assert len(rows) == 1
assert rows[0].rw_id == 42
assert rows[0].rw_uuid == LEGACY_UUID
async def test_renewal_rejects_rename_before_patch():
def handler(request):
assert request.method == 'GET'
return httpx.Response(200, json={'response': user_payload()})
api, http = client_for(handler)
async with http:
with pytest.raises(ValueError, match='rename'):
await api.update_user({'uuid': LEGACY_UUID, 'username': 'other-user'})
async def test_startup_preserves_deleted_cache_but_never_routes_it_to_numeric_id():
deleted_uuid='b2222222-2222-4222-8222-222222222222'
def handler(request):
assert request.method=='GET' and request.url.path=='/api/users/stream'
return httpx.Response(200, json={'response': {'users':[user_payload()], 'hasMore':False,'nextCursor':None}})
api,http=client_for(handler)
async with http:
assert await api.validate_cached_identities([(42,LEGACY_UUID),(7,deleted_uuid)])==1
with pytest.raises(RemnawaveApiError) as error:
await api.update_user({'uuid':deleted_uuid,'status':'DISABLED'})
assert error.value.status_code==404
async def test_startup_fails_when_deleted_cached_id_is_present_in_remote_stream():
def handler(request):
return httpx.Response(200, json={'response': {'users':[user_payload(7)],'hasMore':False,'nextCursor':None}})
api,http=client_for(handler)
async with http:
with pytest.raises(ValueError, match='cache'):
await api.validate_cached_identities([(7,'b2222222-2222-4222-8222-222222222222')])