Skip to content
This repository has been archived by the owner on Jun 14, 2024. It is now read-only.

Commit

Permalink
feat: migrate DELETE /api/users/:user_id endpoint to `DELETE /api/v…
Browse files Browse the repository at this point in the history
…1/users/:user_id` (#148)

# Description

This PR migrate adds a new endpoint `DELETE /api/v1/users/:user_id`
allowing the deletion of users using API v1.

Closes #147 

**Type of change**

(Please delete options that are not relevant. Remember to title the PR
according to the type of change)

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Refactor (change restructuring the codebase without changing
functionality)
- [ ] Improvement (change adding some improvement to an existing
functionality)
- [ ] Documentation update

**How Has This Been Tested**

- [x] Adding new tests and checking that old tests for API v0 pass with
API v1 implementation.

**Checklist**

- [ ] I added relevant documentation
- [ ] follows the style guidelines of this project
- [ ] I did a self-review of my code
- [ ] I made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] I filled out [the contributor form](https://tally.so/r/n9XrxK)
(see text above)
- [ ] I have added relevant notes to the CHANGELOG.md file (See
https://keepachangelog.com/)
  • Loading branch information
jfcalvo authored May 13, 2024
1 parent 89d09e7 commit 2ab1d19
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ These are the section headers that we use:
- Added `GET /api/v1/me` endpoint to get the current user information. ([#140](https://github.com/argilla-io/argilla-server/pull/140))
- Added `GET /api/v1/users` endpoint to get a list of all users. ([#142](https://github.com/argilla-io/argilla-server/pull/142))
- Added `POST /api/v1/users` endpoint to create a new user. ([#146](https://github.com/argilla-io/argilla-server/pull/146))
- Added `DELETE /api/v1/users` endpoint to delete a user. ([#148](https://github.com/argilla-io/argilla-server/pull/148))

## [Unreleased]()

Expand Down
21 changes: 21 additions & 0 deletions src/argilla_server/apis/v1/handlers/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,27 @@ async def create_user(
return user


@router.delete("/users/{user_id}", response_model=User)
async def delete_user(
*,
db: AsyncSession = Depends(get_async_db),
user_id: UUID,
current_user: models.User = Security(auth.get_current_user),
):
user = await accounts.get_user_by_id(db, user_id)
if user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User with id `{user_id}` not found",
)

await authorize(current_user, UserPolicyV1.delete)

await accounts.delete_user(db, user)

return user


@router.get("/users/{user_id}/workspaces", response_model=Workspaces)
async def list_user_workspaces(
*,
Expand Down
4 changes: 4 additions & 0 deletions src/argilla_server/policies.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ async def list(cls, actor: User) -> bool:
async def create(cls, actor: User) -> bool:
return actor.is_owner

@classmethod
async def delete(cls, actor: User) -> bool:
return actor.is_owner

@classmethod
async def list_workspaces(cls, actor: User) -> bool:
return actor.is_owner
Expand Down
82 changes: 82 additions & 0 deletions tests/unit/api/v1/users/test_delete_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Copyright 2021-present, the Recognai S.L. team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from datetime import datetime
from uuid import UUID, uuid4

import pytest
from argilla_server.constants import API_KEY_HEADER_NAME
from argilla_server.enums import UserRole
from argilla_server.models import User
from httpx import AsyncClient
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession

from tests.factories import UserFactory


@pytest.mark.asyncio
class TestDeleteUser:
def url(self, user_id: UUID) -> str:
return f"/api/v1/users/{user_id}"

async def test_delete_user(self, db: AsyncSession, async_client: AsyncClient, owner_auth_header: dict):
user = await UserFactory.create()

response = await async_client.delete(self.url(user.id), headers=owner_auth_header)

assert response.status_code == 200
assert response.json() == {
"id": str(user.id),
"first_name": user.first_name,
"last_name": user.last_name,
"username": user.username,
"role": user.role,
"api_key": user.api_key,
"inserted_at": user.inserted_at.isoformat(),
"updated_at": user.updated_at.isoformat(),
}

assert (await db.execute(select(func.count(User.id)))).scalar() == 1

async def test_delete_user_without_authentication(self, db: AsyncSession, async_client: AsyncClient):
user = await UserFactory.create()

response = await async_client.delete(self.url(user.id))

assert response.status_code == 401
assert (await db.execute(select(func.count(User.id)))).scalar() == 1

@pytest.mark.parametrize("user_role", [UserRole.admin, UserRole.annotator])
async def test_delete_user_with_unauthorized_role(
self, db: AsyncSession, async_client: AsyncClient, user_role: UserRole
):
user = await UserFactory.create()
user_with_unauthorized_role = await UserFactory.create(role=user_role)

response = await async_client.delete(
self.url(user.id),
headers={API_KEY_HEADER_NAME: user_with_unauthorized_role.api_key},
)

assert response.status_code == 403
assert (await db.execute(select(func.count(User.id)))).scalar() == 2

async def test_delete_user_with_nonexistent_user_id(self, async_client: AsyncClient, owner_auth_header: dict):
user_id = uuid4()

response = await async_client.delete(self.url(user_id), headers=owner_auth_header)

assert response.status_code == 404
assert response.json() == {"detail": f"User with id `{user_id}` not found"}

0 comments on commit 2ab1d19

Please sign in to comment.