From dad23733e89bf9e78a12861dbb21f7ceeb8da9e0 Mon Sep 17 00:00:00 2001 From: luxferre Date: Mon, 15 Jun 2026 11:10:02 +0100 Subject: [PATCH] feat: group name unique per org Instead of group names being wholly unique (enforced by the db), group names are unique within the org (enforced by endpoint logic). --- .../2026-06-15_group_name_unique_per_org.py | 34 +++++++++++++++++++ src/iam/models.py | 10 +++++- test/test_iam.py | 9 +++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 .alembic/versions/2026-06-15_group_name_unique_per_org.py diff --git a/.alembic/versions/2026-06-15_group_name_unique_per_org.py b/.alembic/versions/2026-06-15_group_name_unique_per_org.py new file mode 100644 index 0000000..9a8e7f9 --- /dev/null +++ b/.alembic/versions/2026-06-15_group_name_unique_per_org.py @@ -0,0 +1,34 @@ +"""group name unique per org + +Revision ID: 98e20aae555c +Revises: b6c8614ef799 +Create Date: 2026-06-15 11:05:16.673658 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '98e20aae555c' +down_revision: Union[str, Sequence[str], None] = 'b6c8614ef799' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(op.f('group_name_key'), 'group', type_='unique') + op.create_unique_constraint('uniq_group_name_org_id', 'group', ['name', 'org_id']) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint('uniq_group_name_org_id', 'group', type_='unique') + op.create_unique_constraint(op.f('group_name_key'), 'group', ['name'], postgresql_nulls_not_distinct=False) + # ### end Alembic commands ### diff --git a/src/iam/models.py b/src/iam/models.py index 0087abc..5e9b821 100644 --- a/src/iam/models.py +++ b/src/iam/models.py @@ -56,10 +56,18 @@ class Permission(Base): class Group(Base): __tablename__ = "group" id = Column(Integer, primary_key=True) - name = Column(String, nullable=False, unique=True) + name = Column(String, nullable=False) org_id = Column(Integer, ForeignKey("organisation.id", ondelete="CASCADE")) + __table_args__ = ( + UniqueConstraint( + "name", + "org_id", + name="uniq_group_name_org_id", + ), + ) + user_rel = relationship("User", secondary="user_groups", back_populates="group_rel") org_rel = relationship("Organisation", back_populates="group_rel") diff --git a/test/test_iam.py b/test/test_iam.py index 3980eea..b9e597c 100644 --- a/test/test_iam.py +++ b/test/test_iam.py @@ -283,6 +283,15 @@ async def test_post_group_conflict(default_client: AsyncClient): assert resp.status_code == 409 +@pytest.mark.anyio +async def test_post_group_non_conflict(default_client: AsyncClient): + resp = await default_client.post( + "/iam/group", json={"organisation_id": 2, "name": "Org One Group"} + ) + + assert resp.status_code == 201 + + @pytest.mark.anyio async def test_put_group_perm_success(default_client: AsyncClient): resp = await default_client.put(