feat: delete endpoint queries
Some checks failed
ci / lint_and_test (push) Failing after 5s

Delete endpoints do not fully support bodies. Queries used instead.

Tests added.

Resolves #20
This commit is contained in:
Chris Milne 2026-06-09 09:09:41 +01:00
parent e9b272811f
commit c452c6c0d5
13 changed files with 114 additions and 57 deletions

View file

@ -61,3 +61,16 @@ def get_perm_model_body(
perm_model_body_dependency = Annotated[type[Permission], Depends(get_perm_model_body)] perm_model_body_dependency = Annotated[type[Permission], Depends(get_perm_model_body)]
def get_perm_model_query(
db: db_dependency, perm_id: Annotated[int, Query(gt=0)]
) -> type[Permission]:
perm_model = db.get(Permission, perm_id)
if perm_model is None:
raise PermNotFoundException(perm_id)
return perm_model
perm_model_query_dependency = Annotated[type[Permission], Depends(get_perm_model_query)]

View file

@ -31,7 +31,10 @@ from src.auth.dependencies import (
super_admin_dependency, super_admin_dependency,
) )
from src.user.models import User from src.user.models import User
from src.user.dependencies import user_model_body_dependency from src.user.dependencies import (
user_model_body_dependency,
user_model_query_dependency,
)
from src.organisation.models import Organisation as Org from src.organisation.models import Organisation as Org
from src.service.models import Service from src.service.models import Service
@ -46,6 +49,7 @@ from src.iam.dependencies import (
group_model_query_dependency, group_model_query_dependency,
group_model_body_dependency, group_model_body_dependency,
perm_model_body_dependency, perm_model_body_dependency,
perm_model_query_dependency,
) )
from src.iam.schemas import ( from src.iam.schemas import (
IAMGetGroupPermissionsResponse, IAMGetGroupPermissionsResponse,
@ -57,14 +61,11 @@ from src.iam.schemas import (
IAMPutGroupPermissionResponse, IAMPutGroupPermissionResponse,
IAMPutGroupUserRequest, IAMPutGroupUserRequest,
IAMPutGroupUserResponse, IAMPutGroupUserResponse,
IAMDeleteGroupPermissionRequest,
IAMDeleteGroupPermissionResponse, IAMDeleteGroupPermissionResponse,
IAMDeleteGroupUserRequest,
IAMDeleteGroupUserResponse, IAMDeleteGroupUserResponse,
IAMGetPermissionsResponse, IAMGetPermissionsResponse,
IAMPostPermissionRequest, IAMPostPermissionRequest,
IAMPostPermissionResponse, IAMPostPermissionResponse,
IAMDeletePermissionRequest,
IAMGetPermissionsSearchRequest, IAMGetPermissionsSearchRequest,
IAMGetPermissionsSearchResponse, IAMGetPermissionsSearchResponse,
) )
@ -205,10 +206,9 @@ async def add_group_user(
@router.delete("/group/permissions") @router.delete("/group/permissions")
async def remove_group_permissions( async def remove_group_permissions(
db: db_dependency, db: db_dependency,
group_model: group_model_body_dependency, group_model: group_model_query_dependency,
perm_model: perm_model_body_dependency, perm_model: perm_model_query_dependency,
org_model: org_model_root_claim_body_dependency, org_model: org_model_root_claim_query_dependency,
request_model: IAMDeleteGroupPermissionRequest,
): ):
if group_model.org_id != org_model.id: if group_model.org_id != org_model.id:
raise UnauthorizedException("Group does not belong to this organization") raise UnauthorizedException("Group does not belong to this organization")
@ -226,10 +226,9 @@ async def remove_group_permissions(
@router.delete("/group/user") @router.delete("/group/user")
async def remove_group_user( async def remove_group_user(
db: db_dependency, db: db_dependency,
group_model: group_model_body_dependency, group_model: group_model_query_dependency,
user_model: user_model_body_dependency, user_model: user_model_query_dependency,
org_model: org_model_root_claim_body_dependency, org_model: org_model_root_claim_query_dependency,
request_model: IAMDeleteGroupUserRequest,
): ):
if group_model.org_id != org_model.id: if group_model.org_id != org_model.id:
raise UnauthorizedException("Group does not belong to this organization") raise UnauthorizedException("Group does not belong to this organization")
@ -285,8 +284,7 @@ async def create_new_permission(
async def delete_permission( async def delete_permission(
db: db_dependency, db: db_dependency,
su: super_admin_dependency, su: super_admin_dependency,
perm_model: perm_model_body_dependency, perm_model: perm_model_query_dependency,
request_model: IAMDeletePermissionRequest,
): ):
db.delete(perm_model) db.delete(perm_model)
db.commit() db.commit()

View file

@ -81,19 +81,11 @@ class IAMPutGroupUserResponse(CustomBaseModel):
users: list[UserSchema] users: list[UserSchema]
class IAMDeleteGroupPermissionRequest(GroupIDMixin, PermIDMixin):
pass
class IAMDeleteGroupPermissionResponse(CustomBaseModel): class IAMDeleteGroupPermissionResponse(CustomBaseModel):
group: GroupSchema group: GroupSchema
permissions: list[PermissionSchema] permissions: list[PermissionSchema]
class IAMDeleteGroupUserRequest(GroupIDMixin, UserIDMixin):
pass
class IAMDeleteGroupUserResponse(CustomBaseModel): class IAMDeleteGroupUserResponse(CustomBaseModel):
group: GroupSchema group: GroupSchema
users: list[UserSchema] users: list[UserSchema]
@ -112,10 +104,6 @@ class IAMPostPermissionResponse(CustomBaseModel):
permission: PermissionSchema permission: PermissionSchema
class IAMDeletePermissionRequest(PermIDMixin):
pass
class IAMGetPermissionsSearchRequest(OrgIDMixin): class IAMGetPermissionsSearchRequest(OrgIDMixin):
service_id: Annotated[int | None, Field(gt=0)] = None service_id: Annotated[int | None, Field(gt=0)] = None
resource: Optional[str] = None resource: Optional[str] = None

View file

@ -31,6 +31,7 @@ from src.database import db_dependency
from src.user.dependencies import ( from src.user.dependencies import (
user_model_body_dependency, user_model_body_dependency,
user_model_claims_dependency, user_model_claims_dependency,
user_model_query_dependency,
) )
from src.auth.dependencies import ( from src.auth.dependencies import (
super_admin_dependency, super_admin_dependency,
@ -38,7 +39,10 @@ from src.auth.dependencies import (
org_model_root_claim_body_dependency, org_model_root_claim_body_dependency,
) )
from src.organisation.dependencies import org_model_body_dependency from src.organisation.dependencies import (
org_model_body_dependency,
org_model_query_dependency,
)
from src.organisation.constants import ContactType from src.organisation.constants import ContactType
from src.organisation.models import Organisation as Org from src.organisation.models import Organisation as Org
from src.organisation.schemas import ( from src.organisation.schemas import (
@ -52,8 +56,6 @@ from src.organisation.schemas import (
OrgGetOrgResponse, OrgGetOrgResponse,
OrgPatchRootRequest, OrgPatchRootRequest,
OrgGetGroupResponse, OrgGetGroupResponse,
OrgDeleteUserRequest,
OrgDeleteOrgRequest,
OrgPostOrgResponse, OrgPostOrgResponse,
OrgPatchQuestionnaireResponse, OrgPatchQuestionnaireResponse,
OrgPatchStatusResponse, OrgPatchStatusResponse,
@ -324,9 +326,8 @@ async def add_user_to_org(
) )
async def delete_organisation_by_id( async def delete_organisation_by_id(
db: db_dependency, db: db_dependency,
org_model: org_model_body_dependency, org_model: org_model_query_dependency,
su: super_admin_dependency, su: super_admin_dependency,
request_model: OrgDeleteOrgRequest,
): ):
""" """
Removes an organisation from the hub. Removes an organisation from the hub.
@ -411,9 +412,8 @@ async def get_org_groups(org_model: org_model_root_claim_query_dependency):
) )
async def remove_user_from_org( async def remove_user_from_org(
db: db_dependency, db: db_dependency,
org_model: org_model_root_claim_body_dependency, org_model: org_model_root_claim_query_dependency,
user_model: user_model_body_dependency, user_model: user_model_query_dependency,
request_model: OrgDeleteUserRequest,
): ):
""" """
Revokes a user's membership in an organisation. Revokes a user's membership in an organisation.

View file

@ -88,10 +88,6 @@ class OrgPostUserResponse(CustomBaseModel):
users: list[str] users: list[str]
class OrgDeleteUserRequest(OrgIDMixin, UserIDMixin):
pass
class OrgPatchRootRequest(OrgIDMixin, UserIDMixin): class OrgPatchRootRequest(OrgIDMixin, UserIDMixin):
pass pass
@ -133,7 +129,3 @@ class OrgGetOrgResponse(CustomBaseModel):
billing_contact: Optional[str] = None billing_contact: Optional[str] = None
security_contact: Optional[str] = None security_contact: Optional[str] = None
intake_questionnaire: Optional[Questionnaire] = None intake_questionnaire: Optional[Questionnaire] = None
class OrgDeleteOrgRequest(OrgIDMixin):
pass

View file

@ -20,7 +20,10 @@ from src.auth.dependencies import (
from src.service.models import Service from src.service.models import Service
from src.service.utils import generate_api_key from src.service.utils import generate_api_key
from src.service.dependencies import service_model_body_dependency from src.service.dependencies import (
service_model_body_dependency,
service_model_query_dependency,
)
from src.service.schemas import ( from src.service.schemas import (
ServiceGetServiceResponse, ServiceGetServiceResponse,
ServicePostServiceRequest, ServicePostServiceRequest,
@ -28,7 +31,6 @@ from src.service.schemas import (
ServiceWithKeySchema, ServiceWithKeySchema,
ServicePatchKeyResponse, ServicePatchKeyResponse,
ServicePatchKeyRequest, ServicePatchKeyRequest,
ServiceDeleteServiceRequest,
) )
router = APIRouter( router = APIRouter(
@ -137,9 +139,8 @@ async def regenerate_api_key(
) )
async def remove_service( async def remove_service(
db: db_dependency, db: db_dependency,
service_model: service_model_body_dependency, service_model: service_model_query_dependency,
su: super_admin_dependency, su: super_admin_dependency,
request_model: ServiceDeleteServiceRequest,
): ):
""" """
Removes a service from the hub. Removes a service from the hub.

View file

@ -45,7 +45,3 @@ class ServicePatchKeyRequest(ServiceIDMixin):
class ServicePatchKeyResponse(CustomBaseModel): class ServicePatchKeyResponse(CustomBaseModel):
service: ServiceWithKeySchema service: ServiceWithKeySchema
class ServiceDeleteServiceRequest(ServiceIDMixin):
pass

View file

@ -11,7 +11,7 @@ Endpoints:
from fastapi import APIRouter from fastapi import APIRouter
from starlette import status from starlette import status
from src.user.schemas import UserResponse, OIDCClaims, UserDeleteUserRequest from src.user.schemas import UserResponse, OIDCClaims
from src.user.dependencies import ( from src.user.dependencies import (
user_model_claims_dependency, user_model_claims_dependency,
user_model_query_dependency, user_model_query_dependency,
@ -92,9 +92,8 @@ async def get_user_by_id(
) )
async def delete_user_by_id( async def delete_user_by_id(
db: db_dependency, db: db_dependency,
user_model: user_model_body_dependency, user_model: user_model_query_dependency,
su: super_admin_dependency, su: super_admin_dependency,
request_model: UserDeleteUserRequest,
): ):
""" """
Deletes the user with the provided ID from the database. This will not remove them from OIDC, and they will be automatically readded on next login. Deletes the user with the provided ID from the database. This will not remove them from OIDC, and they will be automatically readded on next login.

View file

@ -55,7 +55,3 @@ class UserResponse(CustomBaseModel):
class OrgResponse(CustomBaseModel): class OrgResponse(CustomBaseModel):
org_id: int org_id: int
name: str name: str
class UserDeleteUserRequest(UserIDMixin):
pass

View file

@ -723,3 +723,40 @@ async def test_post_perm_search_status_checks(
resp = await default_client.post("/iam/permissions/search", json=body) resp = await default_client.post("/iam/permissions/search", json=body)
assert resp.status_code == expected_status assert resp.status_code == expected_status
@pytest.mark.anyio
async def test_delete_group_permissions_success(default_client: AsyncClient):
resp = await default_client.delete(
"/iam/group/permissions?org_id=1&group_id=1&perm_id=1"
)
data = resp.json()
assert resp.status_code == 200
assert "permissions" in data
assert isinstance(data["permissions"], list)
assert len(data["permissions"]) == 0
assert "group" in data
assert data["group"]["id"] == 1
assert data["group"]["name"] == "Test Group"
@pytest.mark.anyio
async def test_delete_permissions_success(default_client: AsyncClient):
resp = await default_client.delete("/iam/permission?perm_id=1")
assert resp.status_code == 204
@pytest.mark.anyio
async def test_delete_group_users_success(default_client: AsyncClient):
resp = await default_client.delete("/iam/group/user?org_id=1&group_id=1&user_id=1")
data = resp.json()
assert resp.status_code == 200
assert "users" in data
assert isinstance(data["users"], list)
assert len(data["users"]) == 0
assert "group" in data
assert data["group"]["id"] == 1
assert data["group"]["name"] == "Test Group"

View file

@ -491,3 +491,26 @@ async def test_patch_org_contact_status_checks(
resp = await default_client.patch("/org/contact", json=body) resp = await default_client.patch("/org/contact", json=body)
assert resp.status_code == expected_status assert resp.status_code == expected_status
@pytest.mark.anyio
async def test_delete_org_success(default_client: AsyncClient):
resp = await default_client.delete("/org?org_id=1")
assert resp.status_code == 204
@pytest.mark.anyio
async def test_delete_org_users_success(db_session, default_client: AsyncClient):
db_session.add(
User(
email="user@test.org",
first_name="User",
last_name="Test",
oidc_id="abcd-efgh-ijkl-1234",
)
)
db_session.flush()
resp = await default_client.delete("/org/user?org_id=1&user_id=2")
assert resp.status_code == 204

View file

@ -88,3 +88,10 @@ async def test_patch_services_status_checks(
resp = await default_client.patch("/service/key", json=body) resp = await default_client.patch("/service/key", json=body)
assert resp.status_code == expected_status assert resp.status_code == expected_status
@pytest.mark.anyio
async def test_delete_service_success(default_client: AsyncClient):
resp = await default_client.delete("/service/?service_id=1")
assert resp.status_code == 204

View file

@ -45,3 +45,10 @@ async def test_get_user_status_checks(
resp = await default_client.get(f"/user/?{query}") resp = await default_client.get(f"/user/?{query}")
assert resp.status_code == expected_status assert resp.status_code == expected_status
@pytest.mark.anyio
async def test_delete_user_success(default_client: AsyncClient):
resp = await default_client.delete("/user/?user_id=1")
assert resp.status_code == 204