Compare commits

..

No commits in common. "294baadcb71ef5c409df297b57df4af1f265abd4" and "bdba903db16aece22731b1855ca3bc8e6d25f44e" have entirely different histories.

9 changed files with 42 additions and 159 deletions

View file

@ -21,7 +21,6 @@ from sqlalchemy.exc import IntegrityError
from src.iam.exceptions import GroupNotFoundException from src.iam.exceptions import GroupNotFoundException
from src.organisation.exceptions import OrgNotFoundException from src.organisation.exceptions import OrgNotFoundException
from src.schemas import GroupSummary, OrgSummary
from src.service.exceptions import ServiceNotFoundException from src.service.exceptions import ServiceNotFoundException
from src.exceptions import ConflictException from src.exceptions import ConflictException
from src.database import db_dependency from src.database import db_dependency
@ -128,11 +127,7 @@ async def get_group_permissions(
): ):
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")
return { return {"permissions": group_model.permission_rel}
"organisation": org_model,
"group": group_model,
"permissions": group_model.permission_rel,
}
@router.get("/group/users", response_model=IAMGetGroupUsersResponse) @router.get("/group/users", response_model=IAMGetGroupUsersResponse)
@ -142,11 +137,7 @@ async def get_group_users(
): ):
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")
return { return {"users": group_model.user_rel}
"organisation": org_model,
"group": group_model,
"users": group_model.user_rel,
}
@router.post("/group", response_model=IAMPostGroupResponse) @router.post("/group", response_model=IAMPostGroupResponse)
@ -189,8 +180,7 @@ async def add_group_permission(
db.flush() db.flush()
response = IAMPutGroupPermissionResponse( response = IAMPutGroupPermissionResponse(
organisation=OrgSummary(**org_model.__dict__), group=GroupSchema(**group_model.__dict__),
group=GroupSummary(**group_model.__dict__),
permissions=group_model.permission_rel, permissions=group_model.permission_rel,
) )
db.commit() db.commit()

View file

@ -18,9 +18,6 @@ from src.schemas import (
UserIDMixin, UserIDMixin,
PermIDMixin, PermIDMixin,
GroupIDMixin, GroupIDMixin,
GroupSummary,
OrgSummary,
UserSummary,
) )
@ -53,15 +50,11 @@ class IAMCAoRRequest(CustomBaseModel):
class IAMGetGroupPermissionsResponse(CustomBaseModel): class IAMGetGroupPermissionsResponse(CustomBaseModel):
organisation: OrgSummary
group: GroupSummary
permissions: list[PermissionSchema] permissions: list[PermissionSchema]
class IAMGetGroupUsersResponse(CustomBaseModel): class IAMGetGroupUsersResponse(CustomBaseModel):
organisation: OrgSummary users: list[UserSchema]
group: GroupSummary
users: list[UserSummary]
class IAMPostGroupRequest(OrgIDMixin): class IAMPostGroupRequest(OrgIDMixin):
@ -77,8 +70,7 @@ class IAMPutGroupPermissionRequest(GroupIDMixin, PermIDMixin, OrgIDMixin):
class IAMPutGroupPermissionResponse(CustomBaseModel): class IAMPutGroupPermissionResponse(CustomBaseModel):
organisation: OrgSummary group: GroupSchema
group: GroupSummary
permissions: list[PermissionSchema] permissions: list[PermissionSchema]

View file

@ -314,10 +314,7 @@ async def add_user_to_org(
raise ConflictException(message="User already a part of this organisation") raise ConflictException(message="User already a part of this organisation")
org_model.user_rel.append(user_model) org_model.user_rel.append(user_model)
db.flush() db.flush()
response = { response = {"users": [user.email for user in org_model.user_rel]}
"organisation": org_model,
"users": [{"id": user.id, "email": user.email} for user in org_model.user_rel],
}
db.commit() db.commit()
return response return response
@ -440,12 +437,7 @@ async def get_org_groups(org_model: org_model_root_claim_query_dependency):
""" """
Returns a list of the names of all IAM groups created by the organisation. Returns a list of the names of all IAM groups created by the organisation.
""" """
return { return {"groups": [group.name for group in org_model.group_rel]}
"organisation": org_model,
"groups": [
{"id": group.id, "name": group.name} for group in org_model.group_rel
],
}
@router.delete( @router.delete(

View file

@ -10,14 +10,7 @@ from typing import Optional
from pydantic import EmailStr, ConfigDict from pydantic import EmailStr, ConfigDict
from src.schemas import ( from src.schemas import CustomBaseModel, OrgIDMixin, UserIDMixin
CustomBaseModel,
OrgIDMixin,
UserIDMixin,
GroupSummary,
OrgSummary,
UserSummary,
)
from src.contact.schemas import ContactModel from src.contact.schemas import ContactModel
from src.organisation.constants import Status, ContactType from src.organisation.constants import Status, ContactType
@ -29,6 +22,11 @@ class Questionnaire(CustomBaseModel):
question_three: Optional[str] = None question_three: Optional[str] = None
class OrgSummary(CustomBaseModel):
id: int
name: str
class ContactSummary(CustomBaseModel): class ContactSummary(CustomBaseModel):
id: int id: int
email: Optional[EmailStr] = None email: Optional[EmailStr] = None
@ -51,7 +49,6 @@ class OrgPostOrgRequest(CustomBaseModel):
class OrgPostOrgResponse(CustomBaseModel): class OrgPostOrgResponse(CustomBaseModel):
id: int
name: str name: str
status: Status status: Status
@ -62,7 +59,6 @@ class OrgPatchQuestionnaireRequest(OrgIDMixin):
class OrgPatchQuestionnaireResponse(CustomBaseModel): class OrgPatchQuestionnaireResponse(CustomBaseModel):
id: int
name: str name: str
intake_questionnaire: Questionnaire intake_questionnaire: Questionnaire
status: Status status: Status
@ -73,7 +69,6 @@ class OrgPatchStatusRequest(OrgIDMixin):
class OrgPatchStatusResponse(CustomBaseModel): class OrgPatchStatusResponse(CustomBaseModel):
id: int
name: str name: str
status: Status status: Status
@ -100,8 +95,7 @@ class OrgPostUserRequest(OrgIDMixin, UserIDMixin):
class OrgPostUserResponse(CustomBaseModel): class OrgPostUserResponse(CustomBaseModel):
organisation: OrgSummary users: list[str]
users: list[UserSummary]
class OrgPatchRootRequest(OrgIDMixin, UserIDMixin): class OrgPatchRootRequest(OrgIDMixin, UserIDMixin):
@ -119,8 +113,7 @@ class OrgGetUserResponse(CustomBaseModel):
class OrgGetGroupResponse(CustomBaseModel): class OrgGetGroupResponse(CustomBaseModel):
organisation: OrgSummary groups: list[str]
groups: list[GroupSummary]
class OrgGetContactResponse(CustomBaseModel): class OrgGetContactResponse(CustomBaseModel):

View file

@ -40,18 +40,3 @@ class ServiceIDMixin(CustomBaseModel):
class UserIDMixin(CustomBaseModel): class UserIDMixin(CustomBaseModel):
user_id: int = Field(gt=0) user_id: int = Field(gt=0)
class OrgSummary(CustomBaseModel):
id: int
name: str
class GroupSummary(CustomBaseModel):
id: int
name: str
class UserSummary(CustomBaseModel):
id: int
email: str

View file

@ -1,8 +1,7 @@
import pytest
from typing import AsyncGenerator from typing import AsyncGenerator
from itertools import combinations from itertools import combinations
from fastapi.routing import APIRoute
import pytest
from httpx import AsyncClient, ASGITransport from httpx import AsyncClient, ASGITransport
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
@ -13,6 +12,7 @@ from src.contact.models import Contact
from src.iam.models import Group, Permission from src.iam.models import Group, Permission
from src.auth.service import get_current_user, get_dev_user from src.auth.service import get_current_user, get_dev_user
from src.auth.dependencies import empty_su_list, get_super_admin_list, testing_su_list from src.auth.dependencies import empty_su_list, get_super_admin_list, testing_su_list
from src.main import app # inited FastAPI app from src.main import app # inited FastAPI app
from src.database import engine, Base, get_db from src.database import engine, Base, get_db
@ -156,22 +156,25 @@ def generate_query_and_status(params) -> list[tuple[str, int]]:
return query_and_status return query_and_status
def get_testable_routes(): # # Produces a text file with method and path for every endpoint in the API
routes = [] # from fastapi.routing import APIRoute
#
for route in app.routes: # def get_testable_routes():
if not isinstance(route, APIRoute): # routes = []
continue #
# for route in app.routes:
for method in route.methods: # if not isinstance(route, APIRoute):
if method in {"HEAD", "OPTIONS"}: # continue
continue #
# for method in route.methods:
routes.append((method, route.path, route.status_code, route.response_model)) # if method in {"HEAD", "OPTIONS"}:
# continue
return routes #
# routes.append((route.path, method))
#
# return routes
#
#
# with open("endpoints.txt", "w") as f: # with open("endpoints.txt", "w") as f:
# for ep in get_testable_routes(): # for ep in get_testable_routes():
# f.write(f"[{ep[0]}]{ep[1]}({ep[2]}) -> {ep[2]}: {ep[3]}\n") # f.write(f"{ep[1]} {ep[0]}\n")

View file

@ -221,18 +221,10 @@ async def test_get_group_users_success(default_client: AsyncClient):
user = data["users"][0] user = data["users"][0]
assert user["id"] == 1 assert user["id"] == 1
assert user["first_name"] == "Admin"
assert user["last_name"] == "Test"
assert user["email"] == "admin@test.com" assert user["email"] == "admin@test.com"
assert "group" in data
assert isinstance(data["group"], dict)
assert data["group"]["id"] == 1
assert data["group"]["name"] == "Test Group"
assert "organisation" in data
assert isinstance(data["organisation"], dict)
assert data["organisation"]["id"] == 1
assert data["organisation"]["name"] == "Test Org"
@pytest.mark.parametrize( @pytest.mark.parametrize(
"query, expected_status", generate_query_and_status(["group_id", "org_id"]) "query, expected_status", generate_query_and_status(["group_id", "org_id"])

View file

@ -265,16 +265,9 @@ async def test_post_org_user_success(default_client: AsyncClient, db_session):
data = resp.json() data = resp.json()
assert "organisation" in data
assert isinstance(data["organisation"], dict)
assert data["organisation"]["id"] == 1
assert data["organisation"]["name"] == "Test Org"
assert "users" in data assert "users" in data
assert isinstance(data["users"], list) assert isinstance(data["users"], list)
assert ( assert "user@test.org" in data["users"]
len([user for user in data["users"] if user["email"] == "user@test.org"]) == 1
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -393,17 +386,9 @@ async def test_get_org_groups_success(default_client: AsyncClient):
data = resp.json() data = resp.json()
assert "organisation" in data
assert isinstance(data["organisation"], dict)
assert data["organisation"]["id"] == 1
assert data["organisation"]["name"] == "Test Org"
assert "groups" in data assert "groups" in data
assert isinstance(data["groups"], list) assert isinstance(data["groups"], list)
group = data["groups"][0] assert "Test Group" in data["groups"]
assert isinstance(group, dict)
assert group["id"] == 1
assert group["name"] == "Test Group"
@pytest.mark.parametrize( @pytest.mark.parametrize(

View file

@ -5,7 +5,6 @@
import pytest import pytest
from httpx import AsyncClient from httpx import AsyncClient
from fastapi.routing import APIRoute
from .conftest import generate_query_and_status from .conftest import generate_query_and_status
@ -144,51 +143,3 @@ async def test_get_self_orgs_success(default_client: AsyncClient):
assert isinstance(org["security_contact"], dict) assert isinstance(org["security_contact"], dict)
assert org["security_contact"]["email"] == "security@test.org" assert org["security_contact"]["email"] == "security@test.org"
assert org["security_contact"]["id"] == 3 assert org["security_contact"]["id"] == 3
@pytest.mark.anyio
async def test_get_self_orgs_dynamic(default_client: AsyncClient):
method = "GET"
path = "/user/self/orgs"
expected_data = {
"organisations": [
{
"organisation_id": 1,
"name": "Test Org",
"status": "approved",
"root_user_email": "admin@test.com",
"owner_contact": {"email": "owner@test.org", "id": 2},
"security_contact": {"email": "security@test.org", "id": 3},
"billing_contact": {"email": "billing@test.org", "id": 1},
"intake_questionnaire": {
"question_one": None,
"question_three": None,
"question_two": "answer two",
},
}
]
}
resp = await default_client.get(path)
route = next(
route
for route in default_client._transport.app.routes
if isinstance(route, APIRoute)
and path in route.path
and method in route.methods
)
assert resp.status_code == route.status_code
if route.status_code == 204:
return
expected_response_schema = route.response_model
data = resp.json()
response_model = expected_response_schema(**data)
assert isinstance(response_model, expected_response_schema)
expected_response_model = expected_response_schema(**expected_data)
assert response_model == expected_response_model