From 52a2feef80f1f7233dbe04b4d2d37e4e613178f8 Mon Sep 17 00:00:00 2001 From: Wilson <103034148+lairwaves@users.noreply.github.com> Date: Fri, 22 May 2026 01:05:49 +0800 Subject: [PATCH 1/2] Add Workflows Engineer to invoice privileged roles --- apps/discord_bot/src/five08/discord_bot/cogs/invoices.py | 4 ++-- tests/unit/test_invoices_cog.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/discord_bot/src/five08/discord_bot/cogs/invoices.py b/apps/discord_bot/src/five08/discord_bot/cogs/invoices.py index 5216da71..42097b58 100644 --- a/apps/discord_bot/src/five08/discord_bot/cogs/invoices.py +++ b/apps/discord_bot/src/five08/discord_bot/cogs/invoices.py @@ -30,7 +30,7 @@ # 1. They have Steering Committee role or above (full access). # 2. They created the invoice (invoice owner matches one of their ERP emails). # 3. They are on the invoice's ERP project roster. -_PRIVILEGED_ROLES = ["Steering Committee"] +_PRIVILEGED_ROLES = ["Steering Committee", "Workflows Engineer"] def _can_view_invoice( @@ -116,7 +116,7 @@ async def validate_invoice_command( ) if not include_all and not emails: await interaction.followup.send( - "Invoice validation is available to Steering Committee members " + "Invoice validation is restricted to privileged roles " "or confirmed ERP project members.", ephemeral=True, ) diff --git a/tests/unit/test_invoices_cog.py b/tests/unit/test_invoices_cog.py index 80df1a57..760a09ac 100644 --- a/tests/unit/test_invoices_cog.py +++ b/tests/unit/test_invoices_cog.py @@ -148,7 +148,7 @@ async def test_validate_invoice_denied_without_erp_identity( cog, mock_member_interaction, mock_doctype, "TEST-SINV-0001" ) sent = mock_member_interaction.followup.send.call_args.args[0] - assert "Steering Committee" in sent + assert "privileged roles" in sent cog.client.get_invoice.assert_not_called() From 866130fc32ced3b646f356b17db2d19ee92ab25f Mon Sep 17 00:00:00 2001 From: Wilson <103034148+lairwaves@users.noreply.github.com> Date: Fri, 22 May 2026 11:04:10 +0800 Subject: [PATCH 2/2] Fix denial message and test assertions to derive from _PRIVILEGED_ROLES - Build the access-denied message dynamically from _PRIVILEGED_ROLES so role names are always in sync without manual updates - Update the header comment to reference _PRIVILEGED_ROLES instead of hard-coding "Steering Committee" - Import _PRIVILEGED_ROLES in tests and assert against it so future role additions are covered automatically - Add Workflows Engineer fixture and access test --- .../src/five08/discord_bot/cogs/invoices.py | 5 ++-- tests/unit/test_invoices_cog.py | 23 +++++++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/apps/discord_bot/src/five08/discord_bot/cogs/invoices.py b/apps/discord_bot/src/five08/discord_bot/cogs/invoices.py index 42097b58..34d63d35 100644 --- a/apps/discord_bot/src/five08/discord_bot/cogs/invoices.py +++ b/apps/discord_bot/src/five08/discord_bot/cogs/invoices.py @@ -27,7 +27,7 @@ STATUS_LABEL = {0: "Draft", 1: "Submitted", 2: "Cancelled"} # Invoice access rules — a caller may validate an invoice if any of these hold: -# 1. They have Steering Committee role or above (full access). +# 1. They have a privileged role (one of _PRIVILEGED_ROLES) for full access. # 2. They created the invoice (invoice owner matches one of their ERP emails). # 3. They are on the invoice's ERP project roster. _PRIVILEGED_ROLES = ["Steering Committee", "Workflows Engineer"] @@ -115,8 +115,9 @@ async def validate_invoice_command( self._resolve_access, interaction ) if not include_all and not emails: + roles_str = " or ".join(_PRIVILEGED_ROLES) await interaction.followup.send( - "Invoice validation is restricted to privileged roles " + f"Invoice validation is restricted to {roles_str} " "or confirmed ERP project members.", ephemeral=True, ) diff --git a/tests/unit/test_invoices_cog.py b/tests/unit/test_invoices_cog.py index 760a09ac..2816037d 100644 --- a/tests/unit/test_invoices_cog.py +++ b/tests/unit/test_invoices_cog.py @@ -4,7 +4,7 @@ import pytest -from five08.discord_bot.cogs.invoices import InvoicesCog +from five08.discord_bot.cogs.invoices import InvoicesCog, _PRIVILEGED_ROLES from five08.clients.erpnext import ERPNextAPIError @@ -45,6 +45,12 @@ def mock_interaction() -> AsyncMock: return _make_interaction(role_names=["Steering Committee"], user_id=1001) +@pytest.fixture +def mock_workflows_engineer_interaction() -> AsyncMock: + """A privileged (Workflows Engineer) caller with full invoice access.""" + return _make_interaction(role_names=["Workflows Engineer"], user_id=1002) + + @pytest.fixture def mock_member_interaction() -> AsyncMock: """A non-privileged caller subject to owner/project access rules.""" @@ -148,10 +154,23 @@ async def test_validate_invoice_denied_without_erp_identity( cog, mock_member_interaction, mock_doctype, "TEST-SINV-0001" ) sent = mock_member_interaction.followup.send.call_args.args[0] - assert "privileged roles" in sent + assert all(role in sent for role in _PRIVILEGED_ROLES) cog.client.get_invoice.assert_not_called() +@pytest.mark.asyncio +async def test_validate_invoice_allowed_for_workflows_engineer( + cog, mock_workflows_engineer_interaction, mock_doctype +): + """Workflows Engineer gets include_all access without needing an ERP identity.""" + cog.client.get_invoice = Mock(return_value=VALID_INVOICE) + await cog.validate_invoice_command.callback( + cog, mock_workflows_engineer_interaction, mock_doctype, "TEST-SINV-0001" + ) + embed = mock_workflows_engineer_interaction.followup.send.call_args.kwargs["embed"] + assert "No issues found" in embed.title + + @pytest.mark.asyncio async def test_validate_invoice_allowed_for_invoice_owner( cog, mock_member_interaction, mock_doctype