Skip to content

Commit 5b51779

Browse files
refactor(tests): remove tests dependecies on each others
1 parent 7a0b883 commit 5b51779

16 files changed

+1208
-745
lines changed

app/utils/jwt.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ async def create_admin_token(username: str, is_sudo=False) -> str:
2929

3030
async def get_admin_payload(token: str) -> dict | None:
3131
try:
32-
payload = jwt.decode(token, await get_secret_key(), algorithms=["HS256"])
32+
payload = jwt.decode(token, await get_secret_key(), algorithms=["HS256"], leeway=5)
3333
username: str = payload.get("sub")
3434
access: str = payload.get("access")
3535
if not username or access not in ("admin", "sudo"):

tests/api/helpers.py

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
from typing import Any, Iterable
5+
from uuid import uuid4
6+
7+
from fastapi import status
8+
from sqlalchemy import select
9+
10+
from tests.api import TestSession, client
11+
from tests.api.sample_data import XRAY_CONFIG
12+
13+
from app.db.models import Admin, User, CoreConfig, Group, UserTemplate
14+
15+
16+
def unique_name(prefix: str) -> str:
17+
return f"{prefix}_{uuid4().hex[:8]}"
18+
19+
20+
def auth_headers(access_token: str) -> dict[str, str]:
21+
return {"Authorization": f"Bearer {access_token}"}
22+
23+
24+
def create_admin(access_token: str, *, username: str | None = None, password: str | None = None, is_sudo: bool = False) -> dict:
25+
username = username or unique_name("admin")
26+
password = password or f"TestAdmin#{uuid4().hex[:8]}"
27+
response = client.post(
28+
"/api/admin",
29+
headers=auth_headers(access_token),
30+
json={"username": username, "password": password, "is_sudo": is_sudo},
31+
)
32+
assert response.status_code == status.HTTP_201_CREATED
33+
data = response.json()
34+
data["password"] = password
35+
return data
36+
37+
38+
def delete_admin(access_token: str, username: str) -> None:
39+
response = client.delete(f"/api/admin/{username}", headers=auth_headers(access_token))
40+
if response.status_code in {status.HTTP_204_NO_CONTENT, status.HTTP_404_NOT_FOUND}:
41+
return
42+
43+
async def _force_remove():
44+
async with TestSession() as session:
45+
result = await session.execute(select(Admin).where(Admin.username == username))
46+
db_admin = result.scalar_one_or_none()
47+
if db_admin:
48+
await session.delete(db_admin)
49+
await session.commit()
50+
51+
asyncio.run(_force_remove())
52+
53+
54+
def create_core(
55+
access_token: str,
56+
*,
57+
name: str | None = None,
58+
config: dict[str, Any] | None = None,
59+
exclude: Iterable[str] | None = None,
60+
fallbacks: Iterable[str] | None = None,
61+
) -> dict:
62+
payload = {
63+
"config": config or XRAY_CONFIG,
64+
"name": name or unique_name("core"),
65+
"exclude_inbound_tags": list(exclude or []),
66+
"fallbacks_inbound_tags": list(fallbacks or ["fallback-A", "fallback-B"]),
67+
}
68+
response = client.post("/api/core", headers=auth_headers(access_token), json=payload)
69+
assert response.status_code == status.HTTP_201_CREATED
70+
return response.json()
71+
72+
73+
def delete_core(access_token: str, core_id: int) -> None:
74+
response = client.delete(f"/api/core/{core_id}", headers=auth_headers(access_token))
75+
if response.status_code in {
76+
status.HTTP_204_NO_CONTENT,
77+
status.HTTP_403_FORBIDDEN, # default core cannot be deleted
78+
status.HTTP_404_NOT_FOUND,
79+
}:
80+
return
81+
82+
async def _force_remove():
83+
async with TestSession() as session:
84+
db_core = await session.get(CoreConfig, core_id)
85+
if db_core:
86+
await session.delete(db_core)
87+
await session.commit()
88+
89+
asyncio.run(_force_remove())
90+
91+
92+
def get_inbounds(access_token: str) -> list[str]:
93+
response = client.get("/api/inbounds", headers=auth_headers(access_token))
94+
if response.status_code == status.HTTP_200_OK:
95+
return response.json()
96+
97+
if response.status_code == status.HTTP_404_NOT_FOUND:
98+
core = create_core(access_token)
99+
try:
100+
response = client.get("/api/inbounds", headers=auth_headers(access_token))
101+
assert response.status_code == status.HTTP_200_OK
102+
return response.json()
103+
finally:
104+
delete_core(access_token, core["id"])
105+
106+
raise AssertionError(f"Unexpected response from /api/inbounds: {response.status_code} {response.text}")
107+
108+
109+
def create_group(access_token: str, *, name: str | None = None, inbound_tags: Iterable[str] | None = None) -> dict:
110+
tags = list(inbound_tags or [])
111+
if not tags:
112+
tags = get_inbounds(access_token)
113+
payload = {
114+
"name": name or unique_name("group"),
115+
"inbound_tags": tags[: min(3, len(tags))],
116+
}
117+
response = client.post("/api/group", headers=auth_headers(access_token), json=payload)
118+
assert response.status_code == status.HTTP_201_CREATED
119+
return response.json()
120+
121+
122+
def delete_group(access_token: str, group_id: int) -> None:
123+
response = client.delete(f"/api/group/{group_id}", headers=auth_headers(access_token))
124+
if response.status_code in {status.HTTP_204_NO_CONTENT, status.HTTP_404_NOT_FOUND}:
125+
return
126+
127+
async def _force_remove():
128+
async with TestSession() as session:
129+
db_group = await session.get(Group, group_id)
130+
if db_group:
131+
await session.delete(db_group)
132+
await session.commit()
133+
134+
asyncio.run(_force_remove())
135+
136+
137+
def create_user(
138+
access_token: str,
139+
*,
140+
username: str | None = None,
141+
group_ids: Iterable[int] | None = None,
142+
payload: dict[str, Any] | None = None,
143+
) -> dict:
144+
body = {
145+
"username": username or unique_name("user"),
146+
"proxy_settings": {},
147+
"data_limit": 1024 * 1024,
148+
"data_limit_reset_strategy": "no_reset",
149+
"status": "active",
150+
}
151+
if payload:
152+
body.update(payload)
153+
if group_ids is not None:
154+
body["group_ids"] = list(group_ids)
155+
response = client.post("/api/user", headers=auth_headers(access_token), json=body)
156+
assert response.status_code == status.HTTP_201_CREATED
157+
return response.json()
158+
159+
160+
def delete_user(access_token: str, username: str) -> None:
161+
response = client.delete(f"/api/user/{username}", headers=auth_headers(access_token))
162+
if response.status_code in {status.HTTP_204_NO_CONTENT, status.HTTP_404_NOT_FOUND}:
163+
return
164+
165+
async def _force_remove():
166+
async with TestSession() as session:
167+
result = await session.execute(select(User).where(User.username == username))
168+
db_user = result.scalar_one_or_none()
169+
if db_user:
170+
await session.delete(db_user)
171+
await session.commit()
172+
173+
asyncio.run(_force_remove())
174+
175+
176+
def create_user_template(
177+
access_token: str,
178+
*,
179+
name: str | None = None,
180+
group_ids: Iterable[int],
181+
data_limit: int = 1024 * 1024 * 1024,
182+
expire_duration: int = 3600,
183+
extra_settings: dict[str, Any] | None = None,
184+
status_value: str = "active",
185+
reset_usages: bool = True,
186+
) -> dict:
187+
payload = {
188+
"name": name or unique_name("user_template"),
189+
"group_ids": list(group_ids),
190+
"data_limit": data_limit,
191+
"expire_duration": expire_duration,
192+
"extra_settings": extra_settings or {"flow": "", "method": None},
193+
"status": status_value,
194+
"reset_usages": reset_usages,
195+
}
196+
response = client.post("/api/user_template", headers=auth_headers(access_token), json=payload)
197+
assert response.status_code == status.HTTP_201_CREATED
198+
return response.json()
199+
200+
201+
def delete_user_template(access_token: str, template_id: int) -> None:
202+
response = client.delete(f"/api/user_template/{template_id}", headers=auth_headers(access_token))
203+
if response.status_code in {status.HTTP_204_NO_CONTENT, status.HTTP_404_NOT_FOUND}:
204+
return
205+
206+
async def _force_remove():
207+
async with TestSession() as session:
208+
db_template = await session.get(UserTemplate, template_id)
209+
if db_template:
210+
await session.delete(db_template)
211+
await session.commit()
212+
213+
asyncio.run(_force_remove())

tests/api/test_b_core.py renamed to tests/api/sample_data.py

Lines changed: 1 addition & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
from fastapi import status
2-
3-
from tests.api import client
4-
5-
xray_config = {
1+
XRAY_CONFIG = {
62
"log": {"loglevel": "info"},
73
"inbounds": [
84
{
@@ -304,115 +300,3 @@
304300
]
305301
},
306302
}
307-
308-
309-
def test_core_create(access_token):
310-
"""Test that the core create route is accessible."""
311-
312-
response = client.post(
313-
url="/api/core",
314-
headers={"Authorization": f"Bearer {access_token}"},
315-
json={
316-
"config": xray_config,
317-
"name": "xray_config",
318-
"exclude_inbound_tags": [],
319-
"fallbacks_inbound_tags": ["fallback-A", "fallback-B"],
320-
},
321-
)
322-
assert response.status_code == status.HTTP_201_CREATED
323-
assert response.json()["config"] == xray_config
324-
assert response.json()["name"] == "xray_config"
325-
for v in response.json()["fallbacks_inbound_tags"]:
326-
assert v in {"fallback-A", "fallback-B"}
327-
assert len(response.json()["fallbacks_inbound_tags"]) == 2
328-
assert len(response.json()["exclude_inbound_tags"]) == 0
329-
330-
331-
def test_core_update(access_token):
332-
"""Test that the core update route is accessible."""
333-
334-
response = client.put(
335-
url="/api/core/1",
336-
headers={"Authorization": f"Bearer {access_token}"},
337-
json={
338-
"config": xray_config,
339-
"name": "xray_config_update",
340-
"exclude_inbound_tags": ["Exclude"],
341-
"fallbacks_inbound_tags": ["fallback-A", "fallback-B", "fallback-C", "fallback-D"],
342-
},
343-
params={"restart_nodes": False},
344-
)
345-
assert response.status_code == status.HTTP_200_OK
346-
assert response.json()["config"] == xray_config
347-
assert response.json()["name"] == "xray_config_update"
348-
for v in response.json()["exclude_inbound_tags"]:
349-
assert v in {"Exclude"}
350-
for v in response.json()["fallbacks_inbound_tags"]:
351-
assert v in {"fallback-A", "fallback-B", "fallback-C", "fallback-D"}
352-
assert len(response.json()["fallbacks_inbound_tags"]) == 4
353-
assert len(response.json()["exclude_inbound_tags"]) == 1
354-
355-
356-
def test_core_get(access_token):
357-
"""Test that the core get route is accessible."""
358-
359-
response = client.get(
360-
url="/api/core/1",
361-
headers={"Authorization": f"Bearer {access_token}"},
362-
)
363-
assert response.status_code == status.HTTP_200_OK
364-
assert response.json()["config"] == xray_config
365-
366-
367-
def test_core_delete_1(access_token):
368-
"""Test that the core delete route is accessible."""
369-
370-
response = client.delete(
371-
url="/api/core/1", headers={"Authorization": f"Bearer {access_token}"}, params={"restart_nodes": True}
372-
)
373-
assert response.status_code == status.HTTP_403_FORBIDDEN
374-
375-
376-
def test_core_delete_2(access_token):
377-
"""Test that the core delete route is accessible."""
378-
379-
response = client.post(
380-
url="/api/core",
381-
headers={"Authorization": f"Bearer {access_token}"},
382-
json={
383-
"config": xray_config,
384-
"name": "xray_config",
385-
"exclude_inbound_tags": [],
386-
"fallbacks_inbound_tags": ["fallback-A", "fallback-B"],
387-
},
388-
)
389-
390-
assert response.status_code == status.HTTP_201_CREATED
391-
assert response.json()["config"] == xray_config
392-
assert response.json()["name"] == "xray_config"
393-
assert len(response.json()["fallbacks_inbound_tags"]) == 2
394-
assert len(response.json()["exclude_inbound_tags"]) == 0
395-
for v in response.json()["fallbacks_inbound_tags"]:
396-
assert v in {"fallback-A", "fallback-B"}
397-
398-
response = client.delete(
399-
url=f"/api/core/{response.json()['id']}",
400-
headers={"Authorization": f"Bearer {access_token}"},
401-
)
402-
assert response.status_code == status.HTTP_204_NO_CONTENT
403-
404-
405-
def test_inbounds_get(access_token):
406-
"""Test that the inbounds get route is accessible."""
407-
408-
response = client.get(
409-
url="/api/inbounds",
410-
headers={"Authorization": f"Bearer {access_token}"},
411-
)
412-
config_tags = [
413-
inbound["tag"] for inbound in xray_config["inbounds"] if inbound["tag"] not in ["fallback-B", "fallback-A"]
414-
]
415-
response_tags = [inbound for inbound in response.json() if "<=>" not in inbound]
416-
assert response.status_code == status.HTTP_200_OK
417-
assert len(response.json()) > 0
418-
assert set(response_tags) == set(config_tags)

0 commit comments

Comments
 (0)