Compare commits

...

4 commits

Author SHA1 Message Date
2d958ba520 ci: forgejo publish yaml
Some checks failed
ci / build_and_publish (push) Failing after -2s
Configured to run Ruff and tests
2026-06-08 12:31:42 +01:00
fff7ccde12 test: super admin dep override
Test user super admin account added via override rather than assumed present.
2026-06-08 11:10:39 +01:00
1aac45eb76 feat: group ids in get user endpoints 2026-06-08 10:51:01 +01:00
903b24d17d ruff: config and initial run 2026-06-08 10:45:38 +01:00
18 changed files with 61 additions and 40 deletions

View file

@ -0,0 +1,23 @@
---
name: ci
on:
push:
branches:
- main
jobs:
build_and_publish:
runs-on: docker
container:
image: ghcr.io/catthehacker/ubuntu:runner-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/ruff-action@v3
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: "0.11.19"
- run: uv python install # Gets Python version from pyproject.toml
- run: uv sync
- run: uv run pytest test
env:
ENVIRONMENT: testing

View file

@ -2,6 +2,11 @@
add-bounds = "major"
exclude-newer = "P2W"
[tool.ruff]
exclude = [
".alembic"
]
[project]
name = "cloud-api"
version = "0.1.0"

View file

@ -66,6 +66,9 @@ def get_super_admin_list():
def empty_su_list():
return []
def testing_su_list():
return ["admin@test.com"]
su_list_dependency = Annotated[list[User], Depends(get_super_admin_list)]
async def user_model_super_admin(user_model: user_model_claims_dependency, super_admin_emails: su_list_dependency):

View file

@ -39,7 +39,7 @@ from src.iam.schemas import IAMGetGroupPermissionsResponse, IAMGetGroupUsersResp
GroupSchema, IAMPostGroupResponse, IAMPutGroupPermissionRequest, IAMPutGroupPermissionResponse, \
IAMPutGroupUserRequest, IAMPutGroupUserResponse, IAMDeleteGroupPermissionRequest, IAMDeleteGroupPermissionResponse, \
IAMDeleteGroupUserRequest, IAMDeleteGroupUserResponse, IAMGetPermissionsResponse, IAMPostPermissionRequest, \
IAMPostPermissionResponse, PermissionSchema, IAMDeletePermissionRequest, IAMGetPermissionsSearchRequest, IAMGetPermissionsSearchResponse
IAMPostPermissionResponse, IAMDeletePermissionRequest, IAMGetPermissionsSearchRequest, IAMGetPermissionsSearchResponse
router = APIRouter(
tags=["IAM"],
@ -195,7 +195,7 @@ async def delete_permission(db: db_dependency, su: super_admin_dependency, perm_
@router.post("/permissions/search", response_model=IAMGetPermissionsSearchResponse)
async def get_permissions(db: db_dependency, org_model: org_model_root_claim_body_dependency, request_model: IAMGetPermissionsSearchRequest):
async def post_permissions(db: db_dependency, org_model: org_model_root_claim_body_dependency, request_model: IAMGetPermissionsSearchRequest):
permission_query = db.query(Perm)
if request_model.service_id is not None:

View file

@ -11,7 +11,7 @@ from src.database import db_dependency
from src.schemas import ResourceName
from src.auth.exceptions import UnauthorizedException
from fastapi import HTTPException, status, Request, Depends
from fastapi import Request, Depends
def valid_service_key(db: db_dependency, request: Request, rn: ResourceName) -> bool:

View file

@ -22,7 +22,6 @@ from fastapi.params import Query
from psycopg.errors import UniqueViolation
from sqlalchemy.exc import IntegrityError
from src.auth.exceptions import UnauthorizedException
from src.contact.schemas import ContactModel
from src.exceptions import UnprocessableContentException, ConflictException
from src.contact.models import Contact

View file

@ -42,5 +42,5 @@ class User(Base):
def groups(self):
result = defaultdict(list)
for group in self.group_rel:
result[group.org_rel.name].append(group.name)
result[group.org_rel.name].append({"name": group.name, "id": group.id})
return dict(result)

View file

@ -48,7 +48,7 @@ class UserResponse(CustomBaseModel):
last_name: str
email: str
organisations: list[Optional[dict[str, str|int]]]
groups: Optional[dict[str, list[str]]] = None
groups: Optional[dict[str, list[dict[str, str|int]]]] = None
class OrgResponse(CustomBaseModel):

View file

@ -11,7 +11,7 @@ from src.organisation.models import Organisation as Org
from src.contact.models import Contact
from src.iam.models import Group, Permission
from src.auth.service import get_current_user, get_dev_user
from src.auth.dependencies import empty_su_list, get_super_admin_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.database import engine, Base, get_db
@ -39,6 +39,7 @@ async def default_client(db_session) -> AsyncGenerator[AsyncClient, None]:
return db_session
app.dependency_overrides[get_db] = get_db_override
app.dependency_overrides[get_current_user] = get_dev_user
app.dependency_overrides[get_super_admin_list] = testing_su_list
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://localhost:8000/api/v1") as ac:
yield ac

View file

@ -6,8 +6,6 @@ Delete endpoints are currently skipped because the testing system cannot use bod
import pytest
from httpx import AsyncClient
from .conftest import default_client
from src.organisation.models import Organisation as Org, OrgUsers
from src.user.models import User
from src.iam.models import Group
@ -83,7 +81,7 @@ async def test_get_org_groups_auth_approval(default_client: AsyncClient):
@pytest.mark.anyio
async def test_get_org_contact_auth_approval(default_client: AsyncClient):
resp = await default_client.get(f"/org/contact?org_id=1&contact_type=billing")
resp = await default_client.get("/org/contact?org_id=1&contact_type=billing")
assert resp.status_code != 422
assert resp.status_code == 200

View file

@ -3,11 +3,8 @@
import pytest
from httpx import AsyncClient
from .conftest import no_su_client
from src.organisation.models import Organisation as Org
from src.user.models import User
from src.iam.models import Group
@pytest.mark.anyio

View file

@ -5,8 +5,6 @@ DELETE endpoints are not tested
import pytest
from httpx import AsyncClient
from .conftest import no_su_client
from src.organisation.models import Organisation as Org
from src.user.models import User
from src.iam.models import Group
@ -70,7 +68,7 @@ async def test_get_org_groups_auth_root(no_su_client: AsyncClient):
@pytest.mark.anyio
async def test_get_org_contact_auth_root(no_su_client: AsyncClient):
resp = await no_su_client.get(f"/org/contact?org_id=2&contact_type=billing")
resp = await no_su_client.get("/org/contact?org_id=2&contact_type=billing")
assert resp.status_code != 422
assert resp.status_code == 401
assert "Must be the org's root user" in resp.json()["detail"]

View file

@ -4,8 +4,6 @@ This testing module removes the testing user override to verify that endpoints w
import pytest
from httpx import AsyncClient
from .conftest import no_user_client
@pytest.mark.anyio
async def test_get_self_db_auth_user(no_user_client: AsyncClient):

View file

@ -1,7 +1,6 @@
import pytest
from httpx import AsyncClient
from .conftest import default_client
@pytest.mark.anyio
async def test_healthcheck(default_client: AsyncClient):

View file

@ -7,7 +7,7 @@ from src.user.models import User
from src.organisation.models import Organisation as Org
from src.iam.models import Group
from .conftest import default_client, db_session, generate_query_and_status
from .conftest import generate_query_and_status
@pytest.mark.anyio
@ -25,7 +25,7 @@ async def test_post_act_on_resource_endpoint_success(default_client: AsyncClient
data = resp.json()
assert resp.status_code == 200
assert data == True
assert data is True
@pytest.mark.parametrize(
@ -133,7 +133,7 @@ async def test_get_group_permissions_success(default_client: AsyncClient):
assert resp.status_code == 200
assert "permissions" in data
assert type(data["permissions"]) == list
assert isinstance(data["permissions"], list)
assert data["permissions"][0]["service_name"] == "Test Service"
assert data["permissions"][0]["resource"] == "test_resource"
assert data["permissions"][0]["action"] == "read"
@ -175,7 +175,7 @@ async def test_get_group_users_success(default_client: AsyncClient):
assert resp.status_code == 200
assert "users" in data
assert type(data["users"]) == list
assert isinstance(data["users"], list)
assert data["users"][0]["id"] == 1
assert data["users"][0]["first_name"] == "Admin"
assert data["users"][0]["last_name"] == "Test"
@ -218,7 +218,7 @@ async def test_post_group_success(default_client: AsyncClient):
assert resp.status_code == 200
assert "group" in data
assert type(data["group"]) == dict
assert isinstance(data["group"], dict)
assert data["group"]["name"] == "New Group"
assert data["group"]["id"] == 2
@ -255,12 +255,12 @@ async def test_put_group_perm_success(default_client: AsyncClient, db_session):
assert resp.status_code == 200
assert "group" in data
assert type(data["group"]) == dict
assert isinstance(data["group"], dict)
assert data["group"]["name"] == "Test Group Two"
assert data["group"]["id"] == 2
assert "permissions" in data
assert type(data["permissions"]) == list
assert isinstance(data["permissions"], list)
assert data["permissions"][0]["service_name"] == "Test Service"
assert data["permissions"][0]["resource"] == "test_resource"
assert data["permissions"][0]["action"] == "read"
@ -316,7 +316,7 @@ async def test_put_group_perm_mismatch(default_client: AsyncClient, db_session,
db_session.add(Org(name="Another Test Org", root_user_id=1, billing_contact_id=1, owner_contact_id=2, security_contact_id=3, status="approved"))
db_session.add(Group(name="Another Test Group", org_id=2))
db_session.flush()
resp = await default_client.put(f"/iam/group/permission", json=body)
resp = await default_client.put("/iam/group/permission", json=body)
assert resp.status_code == 401
assert resp.json()["detail"] == "Group does not belong to this organization"
@ -332,12 +332,12 @@ async def test_put_group_user_success(default_client: AsyncClient, db_session):
assert resp.status_code == 200
assert "group" in data
assert type(data["group"]) == dict
assert isinstance(data["group"], dict)
assert data["group"]["name"] == "Test Group"
assert data["group"]["id"] == 1
assert "users" in data
assert type(data["users"]) == list
assert isinstance(data["users"], list)
assert data["users"][1]["id"] == 2
assert data["users"][1]["first_name"] == "User"
assert data["users"][1]["last_name"] == "Test"
@ -387,7 +387,7 @@ async def test_get_permissions_success(default_client: AsyncClient):
assert resp.status_code == 200
assert "permissions" in data
assert type(data["permissions"]) == list
assert isinstance(data["permissions"], list)
assert data["permissions"][0]["service_name"] == "Test Service"
assert data["permissions"][0]["resource"] == "test_resource"
assert data["permissions"][0]["action"] == "read"
@ -470,7 +470,7 @@ async def test_post_perm_search_success(default_client: AsyncClient, db_session,
assert resp.status_code == 200
assert "permissions" in data
assert type(data["permissions"]) == list
assert isinstance(data["permissions"], list)
assert data["permissions"][0]["service_name"] == "Test Service"
assert data["permissions"][0]["resource"] == "test_resource"
assert data["permissions"][0]["action"] == "read"

View file

@ -6,7 +6,7 @@ from httpx import AsyncClient
from src.organisation.models import Organisation, OrgUsers
from src.user.models import User
from .conftest import default_client, generate_query_and_status
from .conftest import generate_query_and_status
@pytest.mark.anyio
@ -151,7 +151,7 @@ async def test_get_org_users_success(default_client: AsyncClient):
assert resp.status_code == 200
assert "users" in data
assert type(data["users"]) == list
assert isinstance(data["users"], list)
assert len(data["users"]) == 1
assert data["users"][0] == "admin@test.com"
@ -181,7 +181,7 @@ async def test_post_org_user_success(default_client: AsyncClient, db_session):
assert resp.status_code == 200
assert "users" in data
assert type(data["users"]) == list
assert isinstance(data["users"], list)
assert "user@test.org" in data["users"]
@ -264,7 +264,7 @@ async def test_get_org_groups_success(default_client: AsyncClient):
assert resp.status_code == 200
assert "groups" in data
assert type(data["groups"]) == list
assert isinstance(data["groups"], list)
assert "Test Group" in data["groups"]
@ -379,7 +379,7 @@ async def test_patch_org_contact_success(default_client: AsyncClient, key: str,
],
)
@pytest.mark.anyio
async def test_patch_org_status_status_checks(default_client: AsyncClient, body: dict[str, str], expected_status: int):
async def test_patch_org_contact_status_checks(default_client: AsyncClient, body: dict[str, str], expected_status: int):
resp = await default_client.patch("/org/contact", json=body)
assert resp.status_code == expected_status

View file

@ -4,7 +4,7 @@
import pytest
from httpx import AsyncClient
from .conftest import default_client, generate_query_and_status
from .conftest import generate_query_and_status
@pytest.mark.anyio
@ -38,7 +38,7 @@ async def test_post_service_success(default_client: AsyncClient):
assert "service" in data
assert data["service"]["name"] == "New Test Service"
assert data["service"]["id"] == 2
assert type(data["service"]["api_key"]) == str
assert isinstance(data["service"]["api_key"], str)
@pytest.mark.parametrize(
@ -64,7 +64,7 @@ async def test_patch_service_success(default_client: AsyncClient):
assert "service" in data
assert data["service"]["name"] == "Test Service"
assert data["service"]["id"] == 1
assert type(data["service"]["api_key"]) == str
assert isinstance(data["service"]["api_key"], str)
@pytest.mark.parametrize(

View file

@ -6,7 +6,7 @@
import pytest
from httpx import AsyncClient
from .conftest import default_client, generate_query_and_status
from .conftest import generate_query_and_status
@pytest.mark.anyio
async def test_get_self_db_success(default_client: AsyncClient):