From dd088ef8ff961db65d5851410f2ded8e0b26de00 Mon Sep 17 00:00:00 2001 From: Mohammad Date: Sat, 23 May 2026 17:00:29 +0330 Subject: [PATCH 1/3] refactor: update admin and user endpoints to use IDs instead of usernames --- app/routers/admin.py | 46 ++++++++++++------------ app/routers/node.py | 45 +++++++++++++++++------ app/routers/user.py | 86 ++++++++++++++++++++++---------------------- 3 files changed, 99 insertions(+), 78 deletions(-) diff --git a/app/routers/admin.py b/app/routers/admin.py index f5d49fadb..c482cc35e 100644 --- a/app/routers/admin.py +++ b/app/routers/admin.py @@ -12,9 +12,9 @@ AdminListQuery, AdminModify, AdminSimpleListQuery, - AdminStatus, AdminsResponse, AdminsSimpleResponse, + AdminStatus, AdminUsageQuery, BulkAdminsActionResponse, BulkAdminSelection, @@ -95,19 +95,19 @@ async def create_admin( @router.put( - "/{username}", + "/{id}", response_model=AdminDetails, responses={403: responses._403, 404: responses._404, 409: responses._409}, ) async def modify_admin( - username: str, + id: int, modified_admin: AdminModify, db: AsyncSession = Depends(get_db), current_admin: AdminDetails = Depends(require_permission("admins", "update")), ): """Modify an existing admin's details.""" - return await admin_operator.modify_admin( - db, username=username, modified_admin=modified_admin, current_admin=current_admin + return await admin_operator.modify_admin_by_id( + db, admin_id=id, modified_admin=modified_admin, current_admin=current_admin ) @@ -143,14 +143,14 @@ async def modify_admin_by_id( ) -@router.delete("/{username}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT) async def remove_admin( - username: str, + id: int, db: AsyncSession = Depends(get_db), current_admin: AdminDetails = Depends(require_permission("admins", "delete")), ): """Remove an admin from the database.""" - await admin_operator.remove_admin(db, username=username, current_admin=current_admin) + await admin_operator.remove_admin_by_id(db, admin_id=id, current_admin=current_admin) return {} @@ -206,18 +206,18 @@ async def get_admins_simple( @router.get( - "/{username}/usage", + "/{id}/usage", response_model=UserUsageStatsList, responses={403: responses._403, 404: responses._404}, ) async def get_admin_usage( - username: str, + id: int, query: Annotated[AdminUsageQuery, Depends(get_admin_usage_query)], db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(get_current), ): """Get admin usage aggregated from user traffic.""" - return await admin_operator.get_admin_usage(db, username=username, admin=admin, query=query) + return await admin_operator.get_admin_usage_by_id(db, admin_id=id, admin=admin, query=query) @router.get( @@ -248,14 +248,14 @@ async def get_admin_usage_by_id( return await admin_operator.get_admin_usage_by_id(db, admin_id=admin_id, admin=admin, query=query) -@router.post("/{username}/users/disable", responses={404: responses._404}) +@router.post("/{id}/users/disable", responses={404: responses._404}) async def disable_all_active_users( - username: str, + id: int, db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(require_permission("admins", "update")), ): """Disable all active users under a specific admin.""" - await admin_operator.disable_all_active_users(db, username=username, admin=admin) + await admin_operator.disable_all_active_users_by_id(db, admin_id=id, admin=admin) return {} @@ -279,14 +279,14 @@ async def disable_all_active_users_by_id( return {} -@router.post("/{username}/users/activate", responses={404: responses._404}) +@router.post("/{id}/users/activate", responses={404: responses._404}) async def activate_all_disabled_users( - username: str, + id: int, db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(require_permission("admins", "update")), ): """Activate all disabled users under a specific admin.""" - await admin_operator.activate_all_disabled_users(db, username=username, admin=admin) + await admin_operator.activate_all_disabled_users_by_id(db, admin_id=id, admin=admin) return {} @@ -310,14 +310,14 @@ async def activate_all_disabled_users_by_id( return {} -@router.delete("/{username}/users", responses={403: responses._403, 404: responses._404}) +@router.delete("/{id}/users", responses={403: responses._403, 404: responses._404}) async def remove_all_users( - username: str, + id: int, db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(require_permission("admins", "delete")), ): """Remove all users under a specific admin.""" - deleted = await admin_operator.remove_all_users(db, username=username, admin=admin) + deleted = await admin_operator.remove_all_users_by_id(db, admin_id=id, admin=admin) return {"detail": f"operation has been successfuly done {deleted} users deleted"} @@ -341,14 +341,14 @@ async def remove_all_users_by_id( return {"detail": f"operation has been successfuly done {deleted} users deleted"} -@router.post("/{username}/reset", response_model=AdminDetails, responses={404: responses._404}) +@router.post("/{id}/reset", response_model=AdminDetails, responses={404: responses._404}) async def reset_admin_usage( - username: str, + id: int, db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(require_permission("admins", "reset_usage")), ): """Resets usage of admin.""" - return await admin_operator.reset_admin_usage(db, username=username, admin=admin) + return await admin_operator.reset_admin_usage_by_id(db, admin_id=id, admin=admin) @router.post("/by-username/{username}/reset", response_model=AdminDetails, responses={404: responses._404}) diff --git a/app/routers/node.py b/app/routers/node.py index 68b39212f..dca24ae97 100644 --- a/app/routers/node.py +++ b/app/routers/node.py @@ -353,34 +353,57 @@ async def realtime_nodes_stats(_: AdminDetails = Depends(require_permission("nod return await node_operator.get_nodes_system_stats() -@router.get("/online_stats/{username}/ip", response_model=UserIPListAll) +@router.get("/online_stats/{id}/ip", response_model=UserIPListAll) async def user_online_ip_list_all_nodes( - username: str, db: AsyncSession = Depends(get_db), _: AdminDetails = Depends(require_permission("nodes", "stats")) + id: int, + db: AsyncSession = Depends(get_db), + admin: AdminDetails = Depends(require_permission("nodes", "stats")), ): """Retrieve user ips from all nodes.""" - return await node_operator.get_user_ip_list_all_nodes(db=db, username=username) + db_user = await node_operator.get_validated_user_by_id( + db, + user_id=id, + admin=admin, + scope_resource="nodes", + scope_action="stats", + ) + return await node_operator.get_user_ip_list_all_nodes(db=db, username=db_user.username) -@router.get("/{node_id}/online_stats/{username}", response_model=dict[int, int]) +@router.get("/{node_id}/online_stats/{id}", response_model=dict[int, int]) async def user_online_stats( node_id: int, - username: str, + id: int, db: AsyncSession = Depends(get_db), - _: AdminDetails = Depends(require_permission("nodes", "stats")), + admin: AdminDetails = Depends(require_permission("nodes", "stats")), ): """Retrieve user online stats by node.""" - return await node_operator.get_user_online_stats_by_node(db=db, node_id=node_id, username=username) + db_user = await node_operator.get_validated_user_by_id( + db, + user_id=id, + admin=admin, + scope_resource="nodes", + scope_action="stats", + ) + return await node_operator.get_user_online_stats_by_node(db=db, node_id=node_id, username=db_user.username) -@router.get("/{node_id}/online_stats/{username}/ip", response_model=UserIPList) +@router.get("/{node_id}/online_stats/{id}/ip", response_model=UserIPList) async def user_online_ip_list( node_id: int, - username: str, + id: int, db: AsyncSession = Depends(get_db), - _: AdminDetails = Depends(require_permission("nodes", "stats")), + admin: AdminDetails = Depends(require_permission("nodes", "stats")), ): """Retrieve user ips by node.""" - return await node_operator.get_user_ip_list_by_node(db=db, node_id=node_id, username=username) + db_user = await node_operator.get_validated_user_by_id( + db, + user_id=id, + admin=admin, + scope_resource="nodes", + scope_action="stats", + ) + return await node_operator.get_user_ip_list_by_node(db=db, node_id=node_id, username=db_user.username) @router.delete( diff --git a/app/routers/user.py b/app/routers/user.py index 91b9a96e8..e9fa24e67 100644 --- a/app/routers/user.py +++ b/app/routers/user.py @@ -13,10 +13,10 @@ from app.models.user import ( BulkUser, BulkUsersActionResponse, + BulkUsersApplyTemplate, BulkUsersCreateResponse, BulkUsersFromTemplate, BulkUsersProxy, - BulkUsersApplyTemplate, BulkUsersSelection, BulkUsersSetOwner, BulkWireGuardPeerIPs, @@ -29,12 +29,12 @@ UserModify, UserResponse, UserSimpleListQuery, - UserUsageQuery, UsersResponse, UsersSimpleResponse, - UsersUsageQuery, UserSubscriptionUpdateChart, UserSubscriptionUpdateList, + UsersUsageQuery, + UserUsageQuery, WireGuardPeerIPsReallocateResponse, ) from app.operation import OperatorType @@ -42,6 +42,8 @@ from app.operation.subscription import SubscriptionOperation from app.operation.user import UserOperation from app.utils import responses + +from .authentication import require_permission, require_scope_all from .dependencies import ( get_expired_users_query, get_user_list_query, @@ -50,8 +52,6 @@ get_users_usage_query, ) -from .authentication import require_permission, require_scope_all - user_operator = UserOperation(operator_type=OperatorType.API) node_operator = NodeOperation(operator_type=OperatorType.API) subscription_operator = SubscriptionOperation(operator_type=OperatorType.API) @@ -89,12 +89,12 @@ async def create_user( @router.put( - "/{username}", + "/{id}", response_model=UserResponse, responses={400: responses._400, 403: responses._403, 404: responses._404}, ) async def modify_user( - username: str, + id: int, modified_user: UserModify, db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(require_permission("users", "update")), @@ -102,7 +102,7 @@ async def modify_user( """ Modify an existing user - - **username**: Cannot be changed. Used to identify the user. + - **id**: Cannot be changed. Used to identify the user. - **status**: User's new status. Can be 'active', 'disabled', 'on_hold', 'limited', or 'expired'. - **expire**: UTC datetime for new account expiration. Set to `0` for unlimited, `null` for no change. - **data_limit**: New max data usage in bytes (e.g., `1073741824` for 1GB). Set to `0` for unlimited, `null` for no change. @@ -116,7 +116,7 @@ async def modify_user( Note: Fields set to `null` or omitted will not be modified. """ - return await user_operator.modify_user(db, username=username, modified_user=modified_user, admin=admin) + return await user_operator.modify_user_by_id(db, user_id=id, modified_user=modified_user, admin=admin) @router.put( @@ -147,16 +147,14 @@ async def modify_user_by_id( return await user_operator.modify_user_by_id(db, user_id=user_id, modified_user=modified_user, admin=admin) -@router.delete( - "/{username}", responses={403: responses._403, 404: responses._404}, status_code=status.HTTP_204_NO_CONTENT -) +@router.delete("/{id}", responses={403: responses._403, 404: responses._404}, status_code=status.HTTP_204_NO_CONTENT) async def remove_user( - username: str, + id: int, db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(require_permission("users", "delete")), ): """Remove a user""" - return await user_operator.remove_user(db, username=username, admin=admin) + return await user_operator.remove_user_by_id(db, user_id=id, admin=admin) @router.delete( @@ -185,14 +183,14 @@ async def remove_user_by_id( return await user_operator.remove_user_by_id(db, user_id=user_id, admin=admin) -@router.post("/{username}/reset", response_model=UserResponse, responses={403: responses._403, 404: responses._404}) +@router.post("/{id}/reset", response_model=UserResponse, responses={403: responses._403, 404: responses._404}) async def reset_user_data_usage( - username: str, + id: int, db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(require_permission("users", "reset_usage")), ): """Reset user data usage""" - return await user_operator.reset_user_data_usage(db, username=username, admin=admin) + return await user_operator.reset_user_data_usage_by_id(db, user_id=id, admin=admin) @router.post( @@ -219,16 +217,14 @@ async def reset_user_data_usage_by_id( return await user_operator.reset_user_data_usage_by_id(db, user_id=user_id, admin=admin) -@router.post( - "/{username}/revoke_sub", response_model=UserResponse, responses={403: responses._403, 404: responses._404} -) +@router.post("/{id}/revoke_sub", response_model=UserResponse, responses={403: responses._403, 404: responses._404}) async def revoke_user_subscription( - username: str, + id: int, db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(require_permission("users", "revoke_sub")), ): """Revoke users subscription (Subscription link and proxies)""" - return await user_operator.revoke_user_sub(db, username=username, admin=admin) + return await user_operator.revoke_user_sub_by_id(db, user_id=id, admin=admin) @router.post( @@ -289,15 +285,15 @@ async def get_users_sub_update_chart( ) -@router.put("/{username}/set_owner", response_model=UserResponse, responses={403: responses._403}) +@router.put("/{id}/set_owner", response_model=UserResponse, responses={403: responses._403}) async def set_owner( - username: str, + id: int, admin_username: str, db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(require_permission("users", "set_owner")), ): """Set a new owner (admin) for a user.""" - return await user_operator.set_owner(db, username=username, admin_username=admin_username, admin=admin) + return await user_operator.set_owner_by_id(db, user_id=id, admin_username=admin_username, admin=admin) @router.put("/by-username/{username}/set_owner", response_model=UserResponse, responses={403: responses._403}) @@ -320,16 +316,14 @@ async def set_owner_by_id( return await user_operator.set_owner_by_id(db, user_id=user_id, admin_username=admin_username, admin=admin) -@router.post( - "/{username}/active_next", response_model=UserResponse, responses={403: responses._403, 404: responses._404} -) +@router.post("/{id}/active_next", response_model=UserResponse, responses={403: responses._403, 404: responses._404}) async def active_next_plan( - username: str, + id: int, db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(require_permission("users", "activate_next_plan")), ): """Reset user by next plan""" - return await user_operator.active_next_plan(db, username=username, admin=admin) + return await user_operator.active_next_plan_by_id(db, user_id=id, admin=admin) @router.post( @@ -356,14 +350,14 @@ async def active_next_plan_by_id( return await user_operator.active_next_plan_by_id(db, user_id=user_id, admin=admin) -@router.get("/{username}", response_model=UserResponse, responses={403: responses._403, 404: responses._404}) +@router.get("/{id}", response_model=UserResponse, responses={403: responses._403, 404: responses._404}) async def get_user( - username: str, + id: int, db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(require_permission("users", "read")), ): """Get user information""" - return await user_operator.get_user(db=db, username=username, admin=admin) + return await user_operator.get_user_by_id(db=db, user_id=id, admin=admin) @router.get( @@ -406,19 +400,25 @@ async def get_user_subscription_by_id( @router.get( - "/{username}/sub_update", + "/{id}/sub_update", response_model=UserSubscriptionUpdateList, responses={403: responses._403, 404: responses._404}, ) async def get_user_sub_update_list( - username: str, + id: int, offset: int = 0, limit: int = 10, db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(require_permission("users", "read")), ): """Get user subscription agent list""" - return await user_operator.get_users_sub_update_list(db, username=username, admin=admin, offset=offset, limit=limit) + return await user_operator.get_users_sub_update_list_by_id( + db, + user_id=id, + admin=admin, + offset=offset, + limit=limit, + ) @router.get( @@ -485,17 +485,15 @@ async def get_users_simple( return await user_operator.get_users_simple(db=db, admin=admin, query=query) -@router.get( - "/{username}/usage", response_model=UserUsageStatsList, responses={403: responses._403, 404: responses._404} -) +@router.get("/{id}/usage", response_model=UserUsageStatsList, responses={403: responses._403, 404: responses._404}) async def get_user_usage( - username: str, + id: int, query: Annotated[UserUsageQuery, Depends(get_user_usage_query)], db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(require_permission("users", "read")), ): """Get users usage""" - return await user_operator.get_user_usage(db, username=username, admin=admin, query=query) + return await user_operator.get_user_usage_by_id(db, user_id=id, admin=admin, query=query) @router.get( @@ -713,14 +711,14 @@ async def bulk_apply_template_to_users( return await user_operator.bulk_apply_template_to_users(db, body, admin) -@router.put("/from_template/{username}", response_model=UserResponse) +@router.put("/from_template/{id}", response_model=UserResponse) async def modify_user_with_template( - username: str, + id: int, modify_template_user: ModifyUserByTemplate, db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(require_permission("users", "update")), ): - return await user_operator.modify_user_with_template(db, username, modify_template_user, admin) + return await user_operator.modify_user_with_template_by_id(db, id, modify_template_user, admin) @router.put("/from_template/by-username/{username}", response_model=UserResponse) From 9350d5967e563377a90889deb3c3626cb972eec4 Mon Sep 17 00:00:00 2001 From: Mohammad Date: Sat, 23 May 2026 17:30:15 +0330 Subject: [PATCH 2/3] fix tests --- tests/api/helpers.py | 24 ++++- tests/api/test_admin.py | 92 +++++++++-------- tests/api/test_bulk.py | 34 +++---- tests/api/test_bulk_delete_entities.py | 52 ++++++---- tests/api/test_bulk_entity_actions.py | 39 ++++--- tests/api/test_user.py | 135 ++++++++++++++++--------- 6 files changed, 228 insertions(+), 148 deletions(-) diff --git a/tests/api/helpers.py b/tests/api/helpers.py index e3039937c..6af9c0e07 100644 --- a/tests/api/helpers.py +++ b/tests/api/helpers.py @@ -51,7 +51,17 @@ def create_admin( def delete_admin(access_token: str, username: str) -> None: - response = client.delete(f"/api/admin/{username}", headers=auth_headers(access_token)) + admin_lookup = client.get( + "/api/admins", + headers=auth_headers(access_token), + params={"username": username}, + ) + assert admin_lookup.status_code == status.HTTP_200_OK + admins = admin_lookup.json()["admins"] + admin = next((item for item in admins if item["username"] == username), None) + assert admin is not None + + response = client.delete(f"/api/admin/{admin['id']}", headers=auth_headers(access_token)) assert response.status_code == status.HTTP_204_NO_CONTENT @@ -212,7 +222,17 @@ def create_user( def delete_user(access_token: str, username: str) -> None: - response = client.delete(f"/api/user/{username}", headers=auth_headers(access_token)) + user_lookup = client.get( + "/api/users", + headers=auth_headers(access_token), + params={"username": username}, + ) + assert user_lookup.status_code == status.HTTP_200_OK + users = user_lookup.json()["users"] + user = next((item for item in users if item["username"] == username), None) + assert user is not None + + response = client.delete(f"/api/user/{user['id']}", headers=auth_headers(access_token)) assert response.status_code == status.HTTP_204_NO_CONTENT diff --git a/tests/api/test_admin.py b/tests/api/test_admin.py index 97b189430..39021715a 100644 --- a/tests/api/test_admin.py +++ b/tests/api/test_admin.py @@ -259,7 +259,7 @@ def test_admin_create_duplicate_telegram_id_conflict(access_token): admin_b_password = strong_password("TestAdminDup") try: response_a = client.put( - url=f"/api/admin/{admin_a['username']}", + url=f"/api/admin/{admin_a['id']}", json={"telegram_id": telegram_id}, headers={"Authorization": f"Bearer {access_token}"}, ) @@ -300,7 +300,7 @@ def test_update_admin(access_token): admin = create_admin(access_token) # role_id=3 (operator) password = strong_password("TestAdminupdate") response = client.put( - url=f"/api/admin/{admin['username']}", + url=f"/api/admin/{admin['id']}", json={ "password": password, "status": "disabled", @@ -313,7 +313,7 @@ def test_update_admin(access_token): # Verify role_id change is applied role_change_response = client.put( - url=f"/api/admin/{admin['username']}", + url=f"/api/admin/{admin['id']}", json={"role_id": 2}, # promote to administrator headers={"Authorization": f"Bearer {access_token}"}, ) @@ -338,7 +338,7 @@ def test_admin_limited_status_not_assignable(access_token): admin = create_admin(access_token) try: update_response = client.put( - url=f"/api/admin/{admin['username']}", + url=f"/api/admin/{admin['id']}", json={"status": "limited"}, headers=auth_headers(access_token), ) @@ -347,16 +347,16 @@ def test_admin_limited_status_not_assignable(access_token): delete_admin(access_token, admin["username"]) -def test_admin_routes_by_id_and_by_username(access_token): +def test_admin_routes_by_id_and_default_id(access_token): admin = create_admin(access_token) try: - by_username_update = client.put( - url=f"/api/admin/by-username/{admin['username']}", - json={"note": "by-username note"}, + default_id_update = client.put( + url=f"/api/admin/{admin['id']}", + json={"note": "default-id note"}, headers=auth_headers(access_token), ) - assert by_username_update.status_code == status.HTTP_200_OK - assert by_username_update.json()["note"] == "by-username note" + assert default_id_update.status_code == status.HTTP_200_OK + assert default_id_update.json()["note"] == "default-id note" by_id_update = client.put( url=f"/api/admin/by-id/{admin['id']}", @@ -373,11 +373,11 @@ def test_admin_routes_by_id_and_by_username(access_token): ) assert by_id_usage.status_code == status.HTTP_200_OK - by_username_reset = client.post( - f"/api/admin/by-username/{admin['username']}/reset", + default_id_reset = client.post( + f"/api/admin/{admin['id']}/reset", headers=auth_headers(access_token), ) - assert by_username_reset.status_code == status.HTTP_200_OK + assert default_id_reset.status_code == status.HTTP_200_OK finally: delete_admin(access_token, admin["username"]) @@ -389,7 +389,7 @@ def test_update_admin_note(access_token): note = "updated admin note" response = client.put( - url=f"/api/admin/{admin['username']}", + url=f"/api/admin/{admin['id']}", json={"note": note}, headers={"Authorization": f"Bearer {access_token}"}, ) @@ -407,14 +407,14 @@ def test_update_admin_duplicate_telegram_id_conflict(access_token): admin_b = create_admin(access_token) try: first_update = client.put( - url=f"/api/admin/{admin_a['username']}", + url=f"/api/admin/{admin_a['id']}", json={"telegram_id": telegram_id}, headers={"Authorization": f"Bearer {access_token}"}, ) assert first_update.status_code == status.HTTP_200_OK second_update = client.put( - url=f"/api/admin/{admin_b['username']}", + url=f"/api/admin/{admin_b['id']}", json={"telegram_id": telegram_id}, headers={"Authorization": f"Bearer {access_token}"}, ) @@ -430,7 +430,7 @@ def test_promote_admin_to_owner_forbidden_via_api(access_token): admin = create_admin(access_token) try: response = client.put( - url=f"/api/admin/{admin['username']}", + url=f"/api/admin/{admin['id']}", json={ "status": "active", "role_id": 1, @@ -469,7 +469,7 @@ def test_administrator_can_modify_self(access_token): administrator_token = login_response.json()["access_token"] response = client.put( - url=f"/api/admin/{administrator_admin['username']}", + url=f"/api/admin/{administrator_admin['id']}", json={ "status": "active", "note": "self-updated", @@ -509,7 +509,7 @@ def test_administrator_cannot_disable_self(access_token): administrator_token = login_response.json()["access_token"] response = client.put( - url=f"/api/admin/{administrator_admin['username']}", + url=f"/api/admin/{administrator_admin['id']}", json={ "status": "disabled", }, @@ -541,7 +541,7 @@ def test_administrator_cannot_modify_other_administrator(access_token): admin_a_token = login_response.json()["access_token"] response = client.put( - url=f"/api/admin/{admin_b['username']}", + url=f"/api/admin/{admin_b['id']}", json={ "status": "active", "note": "should-fail", @@ -699,7 +699,7 @@ def test_disable_admin(access_token): admin = create_admin(access_token) password = admin["password"] disable_response = client.put( - url=f"/api/admin/{admin['username']}", + url=f"/api/admin/{admin['id']}", json={"password": password, "status": "disabled"}, headers={"Authorization": f"Bearer {access_token}"}, ) @@ -738,7 +738,7 @@ def test_admin_delete_all_users_endpoint(access_token): created_users.append(user_name) ownership_response = client.put( - f"/api/user/{user_name}/set_owner", + f"/api/user/{user_response.json()['id']}/set_owner", headers={"Authorization": f"Bearer {access_token}"}, params={"admin_username": admin_username}, ) @@ -746,7 +746,7 @@ def test_admin_delete_all_users_endpoint(access_token): assert ownership_response.json()["admin"]["username"] == admin_username response = client.delete( - url=f"/api/admin/{admin_username}/users", + url=f"/api/admin/{admin['id']}/users", headers={"Authorization": f"Bearer {access_token}"}, ) assert response.status_code == status.HTTP_200_OK @@ -768,7 +768,7 @@ def test_admin_delete(access_token): admin = create_admin(access_token) response = client.delete( - url=f"/api/admin/{admin['username']}", + url=f"/api/admin/{admin['id']}", headers={"Authorization": f"Bearer {access_token}"}, ) assert response.status_code == status.HTTP_204_NO_CONTENT @@ -780,7 +780,7 @@ def test_reset_admin_usage_keeps_lifetime_traffic(access_token): set_admin_used_traffic(admin["username"], 12345) reset_response = client.post( - url=f"/api/admin/{admin['username']}/reset", + url=f"/api/admin/{admin['id']}/reset", headers={"Authorization": f"Bearer {access_token}"}, ) @@ -827,7 +827,7 @@ async def test_admin_usage_returns_stats_for_admin(access_token): await session.commit() response = client.get( - f"/api/admin/{admin['username']}/usage", + f"/api/admin/{admin['id']}/usage", headers=auth_headers(admin_token), params={"period": "hour"}, ) @@ -858,7 +858,7 @@ async def test_admin_usage_forbidden_for_other_admin(access_token): admin_a_token = login_response.json()["access_token"] response = client.get( - f"/api/admin/{admin_b['username']}/usage", + f"/api/admin/{admin_b['id']}/usage", headers=auth_headers(admin_a_token), params={"period": "hour"}, ) @@ -895,12 +895,10 @@ async def test_validate_mini_app_admin_duplicate_telegram_id_conflict(access_tok admin_a = Admin(username=admin_username("mini_dup_a"), hashed_password="secret", telegram_id=telegram_id, role_id=3) admin_b = Admin(username=admin_username("mini_dup_b"), hashed_password="secret", telegram_id=telegram_id, role_id=3) async with TestSession() as session: - session.add_all( - [ - admin_a, - admin_b, - ] - ) + session.add_all([ + admin_a, + admin_b, + ]) await session.commit() async def fake_telegram_settings(): @@ -1283,7 +1281,7 @@ def test_admin_data_limit_set_and_returned(access_token): admin = create_admin(access_token) try: response = client.put( - f"/api/admin/{admin['username']}", + f"/api/admin/{admin['id']}", json={"data_limit": 1073741824}, # 1 GiB headers=auth_headers(access_token), ) @@ -1301,13 +1299,13 @@ def test_admin_data_limit_zero_means_unlimited(access_token): try: # Set a limit first client.put( - f"/api/admin/{admin['username']}", + f"/api/admin/{admin['id']}", json={"data_limit": 1073741824}, headers=auth_headers(access_token), ) # Clear it with 0 response = client.put( - f"/api/admin/{admin['username']}", + f"/api/admin/{admin['id']}", json={"data_limit": 0}, headers=auth_headers(access_token), ) @@ -1336,14 +1334,14 @@ def test_admin_status_becomes_limited_when_traffic_exceeds_limit(access_token): try: # Set data_limit=100 bytes, then simulate traffic=100 bytes client.put( - f"/api/admin/{admin['username']}", + f"/api/admin/{admin['id']}", json={"data_limit": 100}, headers=auth_headers(access_token), ) _set_admin_traffic(admin["username"], used_traffic=100) # Trigger status recompute by calling update_admin with any field response = client.put( - f"/api/admin/{admin['username']}", + f"/api/admin/{admin['id']}", json={"data_limit": 100}, # same value, triggers recompute in CRUD headers=auth_headers(access_token), ) @@ -1358,13 +1356,13 @@ def test_admin_status_returns_to_active_after_limit_raised(access_token): admin = create_admin(access_token) try: # Set limit=100, traffic=100 → limited - client.put(f"/api/admin/{admin['username']}", json={"data_limit": 100}, headers=auth_headers(access_token)) + client.put(f"/api/admin/{admin['id']}", json={"data_limit": 100}, headers=auth_headers(access_token)) _set_admin_traffic(admin["username"], used_traffic=100) - client.put(f"/api/admin/{admin['username']}", json={"data_limit": 100}, headers=auth_headers(access_token)) + client.put(f"/api/admin/{admin['id']}", json={"data_limit": 100}, headers=auth_headers(access_token)) # Raise limit → active response = client.put( - f"/api/admin/{admin['username']}", + f"/api/admin/{admin['id']}", json={"data_limit": 1073741824}, headers=auth_headers(access_token), ) @@ -1379,12 +1377,12 @@ def test_admin_status_returns_to_active_after_reset_usage(access_token): admin = create_admin(access_token) try: # Set limit=100, traffic=100 → limited - client.put(f"/api/admin/{admin['username']}", json={"data_limit": 100}, headers=auth_headers(access_token)) + client.put(f"/api/admin/{admin['id']}", json={"data_limit": 100}, headers=auth_headers(access_token)) _set_admin_traffic(admin["username"], used_traffic=100) - client.put(f"/api/admin/{admin['username']}", json={"data_limit": 100}, headers=auth_headers(access_token)) + client.put(f"/api/admin/{admin['id']}", json={"data_limit": 100}, headers=auth_headers(access_token)) # Reset usage → active - response = client.post(f"/api/admin/{admin['username']}/reset", headers=auth_headers(access_token)) + response = client.post(f"/api/admin/{admin['id']}/reset", headers=auth_headers(access_token)) assert response.status_code == status.HTTP_200_OK assert response.json()["status"] == "active" assert response.json()["used_traffic"] == 0 @@ -1397,7 +1395,7 @@ def test_limited_admin_write_blocked_by_default(access_token): admin = create_admin(access_token) try: # Set limit and exceed it - client.put(f"/api/admin/{admin['username']}", json={"data_limit": 100}, headers=auth_headers(access_token)) + client.put(f"/api/admin/{admin['id']}", json={"data_limit": 100}, headers=auth_headers(access_token)) _set_admin_traffic(admin["username"], used_traffic=100) _set_admin_status(admin["username"], "limited") @@ -1424,7 +1422,7 @@ def test_limited_admin_read_allowed_when_disabled_when_limited_false(access_toke """A limited admin can still read when disabled_when_limited=False (default).""" admin = create_admin(access_token) try: - client.put(f"/api/admin/{admin['username']}", json={"data_limit": 100}, headers=auth_headers(access_token)) + client.put(f"/api/admin/{admin['id']}", json={"data_limit": 100}, headers=auth_headers(access_token)) _set_admin_traffic(admin["username"], used_traffic=100) _set_admin_status(admin["username"], "limited") @@ -1468,7 +1466,7 @@ async def _set_role_flag(): admin = create_admin(access_token, role_id=role["id"]) try: - client.put(f"/api/admin/{admin['username']}", json={"data_limit": 100}, headers=auth_headers(access_token)) + client.put(f"/api/admin/{admin['id']}", json={"data_limit": 100}, headers=auth_headers(access_token)) _set_admin_traffic(admin["username"], used_traffic=100) _set_admin_status(admin["username"], "limited") diff --git a/tests/api/test_bulk.py b/tests/api/test_bulk.py index 5a473be81..fc5e55055 100644 --- a/tests/api/test_bulk.py +++ b/tests/api/test_bulk.py @@ -86,7 +86,7 @@ def test_add_groups_to_users(access_token): assert response.status_code == status.HTTP_200_OK for user in users: - response = client.get(f"/api/user/{user['username']}", headers={"Authorization": f"Bearer {access_token}"}) + response = client.get(f"/api/user/{user['id']}", headers={"Authorization": f"Bearer {access_token}"}) assert set(response.json()["group_ids"]) == set(group_ids) finally: cleanup(access_token, core, groups, users) @@ -111,7 +111,7 @@ def test_remove_groups_from_users(access_token): assert response.status_code == status.HTTP_200_OK for user in users: - response = client.get(f"/api/user/{user['username']}", headers={"Authorization": f"Bearer {access_token}"}) + response = client.get(f"/api/user/{user['id']}", headers={"Authorization": f"Bearer {access_token}"}) assert set(response.json()["group_ids"]) == {group_ids[1]} finally: cleanup(access_token, core, groups, users) @@ -238,12 +238,12 @@ def test_bulk_expire_with_range(access_token): # Manually set them to expired status by setting expire date in the past # Note: the API might return slightly different formatted strings, so we use isoformat client.put( - f"/api/user/{user1['username']}", + f"/api/user/{user1['id']}", headers={"Authorization": f"Bearer {access_token}"}, json={"expire": expire1.isoformat()}, ) client.put( - f"/api/user/{user2['username']}", + f"/api/user/{user2['id']}", headers={"Authorization": f"Bearer {access_token}"}, json={"expire": expire2.isoformat()}, ) @@ -265,13 +265,13 @@ def test_bulk_expire_with_range(access_token): assert response.status_code == status.HTTP_200_OK # Verify user1 was updated - resp1 = client.get(f"/api/user/{user1['username']}", headers={"Authorization": f"Bearer {access_token}"}) + resp1 = client.get(f"/api/user/{user1['id']}", headers={"Authorization": f"Bearer {access_token}"}) new_expire1 = dt.fromisoformat(resp1.json()["expire"].replace("Z", "+00:00")) # Should be approximately expire1 + 1 hour assert (new_expire1 - expire1).total_seconds() == 3600 # Verify user2 was NOT updated - resp2 = client.get(f"/api/user/{user2['username']}", headers={"Authorization": f"Bearer {access_token}"}) + resp2 = client.get(f"/api/user/{user2['id']}", headers={"Authorization": f"Bearer {access_token}"}) new_expire2 = dt.fromisoformat(resp2.json()["expire"].replace("Z", "+00:00")) # Should be exactly expire2 (or very close) assert abs((new_expire2 - expire2).total_seconds()) < 1 @@ -303,12 +303,12 @@ def test_bulk_data_limit_with_expire_range_without_expired_status(access_token): ) client.put( - f"/api/user/{user1['username']}", + f"/api/user/{user1['id']}", headers={"Authorization": f"Bearer {access_token}"}, json={"expire": expire1.isoformat()}, ) client.put( - f"/api/user/{user2['username']}", + f"/api/user/{user2['id']}", headers={"Authorization": f"Bearer {access_token}"}, json={"expire": expire2.isoformat()}, ) @@ -328,8 +328,8 @@ def test_bulk_data_limit_with_expire_range_without_expired_status(access_token): ) assert response.status_code == status.HTTP_200_OK - resp1 = client.get(f"/api/user/{user1['username']}", headers={"Authorization": f"Bearer {access_token}"}) - resp2 = client.get(f"/api/user/{user2['username']}", headers={"Authorization": f"Bearer {access_token}"}) + resp1 = client.get(f"/api/user/{user1['id']}", headers={"Authorization": f"Bearer {access_token}"}) + resp2 = client.get(f"/api/user/{user2['id']}", headers={"Authorization": f"Bearer {access_token}"}) assert resp1.json()["data_limit"] == 150 assert resp2.json()["data_limit"] == 200 @@ -403,7 +403,7 @@ def test_bulk_reset_users_usage_by_ids(access_token): for user in users: user_response = client.get( - f"/api/user/{user['username']}", + f"/api/user/{user['id']}", headers={"Authorization": f"Bearer {access_token}"}, ) assert user_response.status_code == status.HTTP_200_OK @@ -453,7 +453,7 @@ def test_bulk_disable_users_by_ids(access_token): for user in users: user_response = client.get( - f"/api/user/{user['username']}", + f"/api/user/{user['id']}", headers={"Authorization": f"Bearer {access_token}"}, ) assert user_response.status_code == status.HTTP_200_OK @@ -488,7 +488,7 @@ def test_bulk_enable_users_by_ids(access_token): for user in users: user_response = client.get( - f"/api/user/{user['username']}", + f"/api/user/{user['id']}", headers={"Authorization": f"Bearer {access_token}"}, ) assert user_response.status_code == status.HTTP_200_OK @@ -507,7 +507,7 @@ def test_bulk_disable_enable_users_ignore_noops(access_token): first_user = users[0] second_user = users[1] disable_single_response = client.put( - f"/api/user/{first_user['username']}", + f"/api/user/{first_user['id']}", headers={"Authorization": f"Bearer {access_token}"}, json={"status": "disabled"}, ) @@ -560,7 +560,7 @@ def test_bulk_set_owner_by_ids(access_token): for user in users: user_response = client.get( - f"/api/user/{user['username']}", + f"/api/user/{user['id']}", headers={"Authorization": f"Bearer {access_token}"}, ) assert user_response.status_code == status.HTTP_200_OK @@ -640,11 +640,11 @@ def test_bulk_wireguard_reallocate_peer_ips_repairs_duplicates(access_token): assert response.json()["updated"] == 1 first_response = client.get( - f"/api/user/{first_user['username']}", + f"/api/user/{first_user['id']}", headers={"Authorization": f"Bearer {access_token}"}, ) second_response = client.get( - f"/api/user/{second_user['username']}", + f"/api/user/{second_user['id']}", headers={"Authorization": f"Bearer {access_token}"}, ) assert first_response.status_code == status.HTTP_200_OK diff --git a/tests/api/test_bulk_delete_entities.py b/tests/api/test_bulk_delete_entities.py index 7b9b25364..6932b8247 100644 --- a/tests/api/test_bulk_delete_entities.py +++ b/tests/api/test_bulk_delete_entities.py @@ -6,8 +6,7 @@ from fastapi import status from sqlalchemy import func, select, update -from app.db.crud.node import create_node as db_create_node -from app.db.crud.node import remove_node as db_remove_node +from app.db.crud.node import create_node as db_create_node, remove_node as db_remove_node from app.db.models import ( Admin, AdminUsageLogs, @@ -82,8 +81,19 @@ async def _get(): def delete_admin_if_present(access_token: str, username: str) -> None: - response = client.delete(f"/api/admin/{username}", headers=auth_headers(access_token)) - assert response.status_code in (status.HTTP_204_NO_CONTENT, status.HTTP_403_FORBIDDEN, status.HTTP_404_NOT_FOUND) + lookup = client.get( + "/api/admins", + headers=auth_headers(access_token), + params={"username": username}, + ) + assert lookup.status_code == status.HTTP_200_OK + admins = lookup.json()["admins"] + admin = next((item for item in admins if item["username"] == username), None) + if admin is None: + return + + response = client.delete(f"/api/admin/{admin['id']}", headers=auth_headers(access_token)) + assert response.status_code in (status.HTTP_204_NO_CONTENT, status.HTTP_403_FORBIDDEN) def create_owner_admin_row() -> dict: @@ -190,22 +200,20 @@ def seed_node_usage_rows(node_id: int, user_id: int) -> None: async def _seed(): async with TestSession() as session: now = datetime.now(timezone.utc) - session.add_all( - [ - NodeUserUsage(user_id=user_id, node_id=node_id, created_at=now, used_traffic=1), - NodeUsage(node_id=node_id, created_at=now + timedelta(minutes=1), uplink=2, downlink=3), - NodeUsageResetLogs(node_id=node_id, uplink=4, downlink=5), - NodeStat( - node_id=node_id, - mem_total=4096, - mem_used=1024, - cpu_cores=4, - cpu_usage=50, - incoming_bandwidth_speed=100, - outgoing_bandwidth_speed=200, - ), - ] - ) + session.add_all([ + NodeUserUsage(user_id=user_id, node_id=node_id, created_at=now, used_traffic=1), + NodeUsage(node_id=node_id, created_at=now + timedelta(minutes=1), uplink=2, downlink=3), + NodeUsageResetLogs(node_id=node_id, uplink=4, downlink=5), + NodeStat( + node_id=node_id, + mem_total=4096, + mem_used=1024, + cpu_cores=4, + cpu_usage=50, + incoming_bandwidth_speed=100, + outgoing_bandwidth_speed=200, + ), + ]) await session.commit() asyncio.run(_seed()) @@ -320,7 +328,7 @@ def test_bulk_delete_admins_clears_owned_users_and_usage_logs(access_token): user = create_user(access_token, payload={"username": unique_name("bulk_admin_user")}) try: owner_response = client.put( - f"/api/user/{user['username']}/set_owner", + f"/api/user/{user['id']}/set_owner", headers=auth_headers(access_token), params={"admin_username": admin["username"]}, ) @@ -517,7 +525,7 @@ def test_bulk_delete_groups_removes_all_associations(access_token): assert response.status_code == status.HTTP_200_OK assert response.json()["count"] == 1 - user_response = client.get(f"/api/user/{user['username']}", headers=auth_headers(access_token)) + user_response = client.get(f"/api/user/{user['id']}", headers=auth_headers(access_token)) assert user_response.status_code == status.HTTP_200_OK assert user_response.json()["group_ids"] == [] assert get_group_association_counts(group["id"]) == {"users": 0, "templates": 0, "inbounds": 0} diff --git a/tests/api/test_bulk_entity_actions.py b/tests/api/test_bulk_entity_actions.py index a9c422c5c..36e462c3f 100644 --- a/tests/api/test_bulk_entity_actions.py +++ b/tests/api/test_bulk_entity_actions.py @@ -5,8 +5,7 @@ from fastapi import status from sqlalchemy import func, select, update -from app.db.crud.node import create_node as db_create_node -from app.db.crud.node import remove_node as db_remove_node +from app.db.crud.node import create_node as db_create_node, remove_node as db_remove_node from app.db.models import Admin, AdminUsageLogs, Node from app.models.node import NodeCreate from tests.api import TestSession, client @@ -117,8 +116,19 @@ def delete_host_if_present(access_token: str, host_id: int) -> None: def delete_user_if_present(access_token: str, username: str) -> None: - response = client.delete(f"/api/user/{username}", headers=auth_headers(access_token)) - assert response.status_code in (status.HTTP_204_NO_CONTENT, status.HTTP_404_NOT_FOUND) + lookup = client.get( + "/api/users", + headers=auth_headers(access_token), + params={"username": username}, + ) + assert lookup.status_code == status.HTTP_200_OK + users = lookup.json()["users"] + user = next((item for item in users if item["username"] == username), None) + if user is None: + return + + response = client.delete(f"/api/user/{user['id']}", headers=auth_headers(access_token)) + assert response.status_code == status.HTTP_204_NO_CONTENT def valid_group_name(prefix: str) -> str: @@ -364,14 +374,14 @@ def test_bulk_admin_user_actions(access_token): try: for user in (active_user, disabled_user): response = client.put( - f"/api/user/{user['username']}/set_owner", + f"/api/user/{user['id']}/set_owner", headers=auth_headers(access_token), params={"admin_username": admin["username"]}, ) assert response.status_code == status.HTTP_200_OK response = client.put( - f"/api/user/{disabled_user['username']}", + f"/api/user/{disabled_user['id']}", headers=auth_headers(access_token), json={"status": "disabled"}, ) @@ -385,9 +395,9 @@ def test_bulk_admin_user_actions(access_token): assert response.status_code == status.HTTP_200_OK assert response.json()["count"] == 1 - active_user_response = client.get(f"/api/user/{active_user['username']}", headers=auth_headers(access_token)) + active_user_response = client.get(f"/api/user/{active_user['id']}", headers=auth_headers(access_token)) disabled_user_response = client.get( - f"/api/user/{disabled_user['username']}", + f"/api/user/{disabled_user['id']}", headers=auth_headers(access_token), ) assert active_user_response.status_code == status.HTTP_200_OK @@ -403,9 +413,9 @@ def test_bulk_admin_user_actions(access_token): assert response.status_code == status.HTTP_200_OK assert response.json()["count"] == 1 - active_user_response = client.get(f"/api/user/{active_user['username']}", headers=auth_headers(access_token)) + active_user_response = client.get(f"/api/user/{active_user['id']}", headers=auth_headers(access_token)) disabled_user_response = client.get( - f"/api/user/{disabled_user['username']}", + f"/api/user/{disabled_user['id']}", headers=auth_headers(access_token), ) assert active_user_response.status_code == status.HTTP_200_OK @@ -423,8 +433,13 @@ def test_bulk_admin_user_actions(access_token): assert response.json()["count"] == 1 for username in (active_user["username"], disabled_user["username"]): - lookup = client.get(f"/api/user/{username}", headers=auth_headers(access_token)) - assert lookup.status_code == status.HTTP_404_NOT_FOUND + lookup = client.get( + "/api/users", + headers=auth_headers(access_token), + params={"username": username}, + ) + assert lookup.status_code == status.HTTP_200_OK + assert lookup.json()["users"] == [] finally: delete_user_if_present(access_token, active_user["username"]) delete_user_if_present(access_token, disabled_user["username"]) diff --git a/tests/api/test_user.py b/tests/api/test_user.py index a2083f3da..a00772510 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -1,23 +1,23 @@ +import asyncio import io import json +import time import zipfile from base64 import b64encode from copy import deepcopy from datetime import datetime, timedelta, timezone from hashlib import sha256 from math import ceil -import asyncio -import time -from urllib.parse import parse_qs, unquote, urlsplit from unittest.mock import AsyncMock, MagicMock +from urllib.parse import parse_qs, unquote, urlsplit from fastapi import status from sqlalchemy import func, select, update from app.db.crud.hwid import register_user_hwid from app.db.models import NodeUserUsage, User -from app.models.stats import Period, UserCountMetric, UserCountMetricStat, UserCountMetricStatsList from app.models.settings import ConfigFormat, SubRule, Subscription +from app.models.stats import Period, UserCountMetric, UserCountMetricStat, UserCountMetricStatsList from app.operation.subscription import SubscriptionOperation from app.utils import jwt as jwt_utils from app.utils.crypto import generate_wireguard_keypair, get_wireguard_public_key @@ -247,23 +247,33 @@ def test_limited_admin_cannot_create_or_modify_user_to_unlimited_data_or_expire( }, ) assert response.status_code == status.HTTP_201_CREATED + user_id = response.json()["id"] - response = client.put(f"/api/user/{username}", headers=auth_headers(admin_token), json={"note": "allowed"}) + response = client.put(f"/api/user/{user_id}", headers=auth_headers(admin_token), json={"note": "allowed"}) assert response.status_code == status.HTTP_200_OK - response = client.put(f"/api/user/{username}", headers=auth_headers(admin_token), json={"data_limit": 0}) + response = client.put(f"/api/user/{user_id}", headers=auth_headers(admin_token), json={"data_limit": 0}) assert response.status_code == status.HTTP_400_BAD_REQUEST assert "Data limit cannot be unlimited" in response.json()["detail"] - response = client.put(f"/api/user/{username}", headers=auth_headers(admin_token), json={"expire": 0}) + response = client.put(f"/api/user/{user_id}", headers=auth_headers(admin_token), json={"expire": 0}) assert response.status_code == status.HTTP_400_BAD_REQUEST assert "Expire cannot be unlimited" in response.json()["detail"] - response = client.put(f"/api/user/{username}", headers=auth_headers(admin_token), json={"hwid_limit": 0}) + response = client.put(f"/api/user/{user_id}", headers=auth_headers(admin_token), json={"hwid_limit": 0}) assert response.status_code == status.HTTP_400_BAD_REQUEST assert "HWID limit cannot be unlimited" in response.json()["detail"] finally: - client.delete(f"/api/user/{username}", headers=auth_headers(access_token)) + user_lookup = client.get( + "/api/users", + headers=auth_headers(access_token), + params={"username": username}, + ) + assert user_lookup.status_code == status.HTTP_200_OK + users = user_lookup.json()["users"] + db_user = next((item for item in users if item["username"] == username), None) + if db_user is not None: + client.delete(f"/api/user/{db_user['id']}", headers=auth_headers(access_token)) delete_admin(access_token, admin["username"]) _delete_role(access_token, role["id"]) @@ -652,14 +662,14 @@ def test_users_get_filters_by_admin_ids(access_token): try: set_owner_a = client.put( - f"/api/user/{user_a['username']}/set_owner", + f"/api/user/{user_a['id']}/set_owner", headers=auth_headers(access_token), params={"admin_username": admin_a["username"]}, ) assert set_owner_a.status_code == status.HTTP_200_OK set_owner_b = client.put( - f"/api/user/{user_b['username']}/set_owner", + f"/api/user/{user_b['id']}/set_owner", headers=auth_headers(access_token), params={"admin_username": admin_b["username"]}, ) @@ -757,7 +767,7 @@ def test_user_subscriptions(access_token): cleanup_groups(access_token, core, groups) -def test_user_routes_by_id_and_by_username(access_token): +def test_user_routes_by_id_and_default_id(access_token): core, groups = setup_groups(access_token, 1) user = create_user(access_token, group_ids=[groups[0]["id"]], payload={"username": unique_name("id_routes_user")}) try: @@ -765,9 +775,9 @@ def test_user_routes_by_id_and_by_username(access_token): assert by_id_get.status_code == status.HTTP_200_OK assert by_id_get.json()["username"] == user["username"] - by_username_get = client.get(f"/api/user/by-username/{user['username']}", headers=auth_headers(access_token)) - assert by_username_get.status_code == status.HTTP_200_OK - assert by_username_get.json()["id"] == user["id"] + default_get = client.get(f"/api/user/{user['id']}", headers=auth_headers(access_token)) + assert default_get.status_code == status.HTTP_200_OK + assert default_get.json()["id"] == user["id"] patch_payload = {"note": "updated via by-id"} by_id_modify = client.put( @@ -778,12 +788,12 @@ def test_user_routes_by_id_and_by_username(access_token): assert by_id_modify.status_code == status.HTTP_200_OK assert by_id_modify.json()["note"] == patch_payload["note"] - by_username_usage = client.get( - f"/api/user/by-username/{user['username']}/usage", + default_usage = client.get( + f"/api/user/{user['id']}/usage", headers=auth_headers(access_token), params={"period": "hour"}, ) - assert by_username_usage.status_code == status.HTTP_200_OK + assert default_usage.status_code == status.HTTP_200_OK finally: delete_user(access_token, user["username"]) cleanup_groups(access_token, core, groups) @@ -923,7 +933,7 @@ def test_user_sub_update_user_agent(access_token): ip = "203.0.113.10" client.get(url, headers={"User-Agent": user_agent, "X-Forwarded-For": ip}) response = client.get( - f"/api/user/{user['username']}/sub_update", + f"/api/user/{user['id']}/sub_update", headers={"Authorization": f"Bearer {access_token}"}, ) assert response.status_code == status.HTTP_200_OK @@ -964,7 +974,7 @@ def test_user_sub_update_user_agent_truncates_long_values(access_token): long_user_agent = "A" * 1000 client.get(url, headers={"User-Agent": long_user_agent}) response = client.get( - f"/api/user/{user['username']}/sub_update", + f"/api/user/{user['id']}/sub_update", headers={"Authorization": f"Bearer {access_token}"}, ) assert response.status_code == status.HTTP_200_OK @@ -1266,13 +1276,11 @@ def test_xray_subscription_uses_host_specific_template_override(access_token): access_token, name=unique_name("xray_host_override_template"), template_type="xray_subscription", - content=json.dumps( - { - "log": {"loglevel": "warning"}, - "inbounds": [{"tag": "placeholder", "protocol": "vmess", "settings": {"clients": []}}], - "outbounds": [{"tag": "template-marker", "protocol": "freedom", "settings": {}}], - } - ), + content=json.dumps({ + "log": {"loglevel": "warning"}, + "inbounds": [{"tag": "placeholder", "protocol": "vmess", "settings": {"clients": []}}], + "outbounds": [{"tag": "template-marker", "protocol": "freedom", "settings": {}}], + }), ) host_response = client.post( @@ -1338,13 +1346,11 @@ def test_xray_subscription_template_override_isolated_per_host(access_token): access_token, name=unique_name("xray_host_isolated_template"), template_type="xray_subscription", - content=json.dumps( - { - "log": {"loglevel": "warning"}, - "inbounds": [{"tag": "placeholder", "protocol": "vmess", "settings": {"clients": []}}], - "outbounds": [{"tag": "template-marker", "protocol": "freedom", "settings": {}}], - } - ), + content=json.dumps({ + "log": {"loglevel": "warning"}, + "inbounds": [{"tag": "placeholder", "protocol": "vmess", "settings": {"clients": []}}], + "outbounds": [{"tag": "template-marker", "protocol": "freedom", "settings": {}}], + }), ) first_host_response = client.post( @@ -1603,7 +1609,7 @@ def test_user_can_be_assigned_to_multiple_wireguard_interfaces(access_token): # Test no-op update preserves allocated peer_ips update_response = client.put( - f"/api/user/{user['username']}", + f"/api/user/{user['id']}", headers=auth_headers(access_token), json={"note": "keep existing wireguard allocations"}, ) @@ -1744,7 +1750,7 @@ def test_shared_wireguard_peer_ips_can_be_applied_to_multiple_interfaces(access_ updated_proxy_settings = deepcopy(user["proxy_settings"]) updated_proxy_settings["wireguard"]["peer_ips"] = updated_shared_peer_ips update_response = client.put( - f"/api/user/{user['username']}", + f"/api/user/{user['id']}", headers=auth_headers(access_token), json={"proxy_settings": updated_proxy_settings}, ) @@ -1849,7 +1855,7 @@ def test_reset_user_usage(access_token): ) try: response = client.post( - f"/api/user/{user['username']}/reset", + f"/api/user/{user['id']}/reset", headers={"Authorization": f"Bearer {access_token}"}, ) assert response.status_code == status.HTTP_200_OK @@ -1915,7 +1921,7 @@ def test_user_update(access_token): ) try: response = client.put( - f"/api/user/{user['username']}", + f"/api/user/{user['id']}", headers={"Authorization": f"Bearer {access_token}"}, json={ "group_ids": [groups[1]["id"]], @@ -1944,13 +1950,13 @@ def test_reset_by_next_user_usage(access_token): ) try: update = client.put( - f"/api/user/{user['username']}", + f"/api/user/{user['id']}", headers={"Authorization": f"Bearer {access_token}"}, json={"next_plan": {"data_limit": 100, "expire": 100, "add_remaining_traffic": True}}, ) assert update.status_code == status.HTTP_200_OK response = client.post( - f"/api/user/{user['username']}/active_next", + f"/api/user/{user['id']}/active_next", headers={"Authorization": f"Bearer {access_token}"}, ) assert response.status_code == status.HTTP_200_OK @@ -1969,7 +1975,7 @@ def test_revoke_user_subscription(access_token): ) try: response = client.post( - f"/api/user/{user['username']}/revoke_sub", + f"/api/user/{user['id']}/revoke_sub", headers={"Authorization": f"Bearer {access_token}"}, ) assert response.status_code == status.HTTP_200_OK @@ -1988,7 +1994,7 @@ def test_user_delete(access_token): ) try: response = client.delete( - f"/api/user/{user['username']}", + f"/api/user/{user['id']}", headers={"Authorization": f"Bearer {access_token}"}, ) assert response.status_code == status.HTTP_204_NO_CONTENT @@ -2026,9 +2032,17 @@ def test_modify_user_with_template(access_token): headers={"Authorization": f"Bearer {access_token}"}, json={"username": username, "user_template_id": template["id"]}, ) + user_lookup = client.get( + "/api/users", + headers={"Authorization": f"Bearer {access_token}"}, + params={"username": username}, + ) + assert user_lookup.status_code == status.HTTP_200_OK + created_user = next((item for item in user_lookup.json()["users"] if item["username"] == username), None) + assert created_user is not None try: response = client.put( - f"/api/user/from_template/{username}", + f"/api/user/from_template/{created_user['id']}", headers={"Authorization": f"Bearer {access_token}"}, json={"user_template_id": template["id"]}, ) @@ -2065,7 +2079,7 @@ async def _seed_user_state(): assert asyncio.run(_seed_user_state()) == 1234 response = client.put( - f"/api/user/from_template/{user['username']}", + f"/api/user/from_template/{user['id']}", headers={"Authorization": f"Bearer {access_token}"}, json={"user_template_id": template["id"]}, ) @@ -2115,7 +2129,18 @@ def test_bulk_create_users_from_template_sequence(access_token): expected_usernames = [f"{base_username}{start_number + idx}" for idx in range(count)] for username in expected_usernames: - user_response = client.get(f"/api/user/{username}", headers={"Authorization": f"Bearer {access_token}"}) + list_response = client.get( + "/api/users", + headers={"Authorization": f"Bearer {access_token}"}, + params={"username": username}, + ) + assert list_response.status_code == status.HTTP_200_OK + created_user = next((item for item in list_response.json()["users"] if item["username"] == username), None) + assert created_user is not None + user_response = client.get( + f"/api/user/{created_user['id']}", + headers={"Authorization": f"Bearer {access_token}"}, + ) assert user_response.status_code == status.HTTP_200_OK assert user_response.json()["data_limit"] == template["data_limit"] assert user_response.json()["status"] == template["status"] @@ -2161,7 +2186,18 @@ def test_bulk_create_users_from_template_sequence_with_template_affixes(access_t expected_usernames = [f"{prefix}{base_username}{suffix}{start_number + idx}" for idx in range(count)] for username in expected_usernames: - user_response = client.get(f"/api/user/{username}", headers={"Authorization": f"Bearer {access_token}"}) + list_response = client.get( + "/api/users", + headers={"Authorization": f"Bearer {access_token}"}, + params={"username": username}, + ) + assert list_response.status_code == status.HTTP_200_OK + created_user = next((item for item in list_response.json()["users"] if item["username"] == username), None) + assert created_user is not None + user_response = client.get( + f"/api/user/{created_user['id']}", + headers={"Authorization": f"Bearer {access_token}"}, + ) assert user_response.status_code == status.HTTP_200_OK assert user_response.json()["data_limit"] == template["data_limit"] assert user_response.json()["status"] == template["status"] @@ -2254,8 +2290,11 @@ def test_bulk_apply_template_to_users(access_token): assert response.status_code == status.HTTP_200_OK assert response.json()["count"] == 2 - for username in (user1["username"], user2["username"]): - user_response = client.get(f"/api/user/{username}", headers={"Authorization": f"Bearer {access_token}"}) + for user_id in (user1["id"], user2["id"]): + user_response = client.get( + f"/api/user/{user_id}", + headers={"Authorization": f"Bearer {access_token}"}, + ) assert user_response.status_code == status.HTTP_200_OK assert user_response.json()["data_limit"] == template["data_limit"] assert user_response.json()["status"] == template["status"] From f936134cb19783a0871243d4904c088722b9ec26 Mon Sep 17 00:00:00 2001 From: Mohammad Date: Sat, 23 May 2026 17:36:52 +0330 Subject: [PATCH 3/3] fix --- tests/api/test_bulk.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/api/test_bulk.py b/tests/api/test_bulk.py index fc5e55055..afaf85439 100644 --- a/tests/api/test_bulk.py +++ b/tests/api/test_bulk.py @@ -6,8 +6,7 @@ from app.db.models import User from app.utils.crypto import generate_wireguard_keypair -from tests.api import TestSession -from tests.api import client +from tests.api import TestSession, client from tests.api.helpers import ( create_admin, create_core, @@ -156,12 +155,12 @@ def test_update_users_expire(access_token): create_user(access_token, group_ids=[groups[0]["id"]], payload={"username": unique_name("user_expire2")}), ] client.put( - f"/api/user/{users[0]['username']}", + f"/api/user/{users[0]['id']}", headers={"Authorization": f"Bearer {access_token}"}, json={"expire": "2025-01-01T00:00:00+00:00"}, ) client.put( - f"/api/user/{users[1]['username']}", + f"/api/user/{users[1]['id']}", headers={"Authorization": f"Bearer {access_token}"}, json={"expire": "2026-01-01T00:00:00+00:00"}, )