Ticketing groups have no delete/archive route (UI or API) #7

Closed
opened 2026-07-26 15:59:27 -07:00 by jwed · 1 comment
Owner

What I did

As part of a regression sweep of the ticketing app's CRUD flow (following
dashboard-e2e's own tests/flows/ticketing/ticketing-crud.spec.js pattern), I:

  1. Logged in as superadmin@playwright.test.
  2. Created an ephemeral ticketing group via POST to ticketing.settings.groups.store
    (name [qa-sweep] Desk, prefix QAS2eeueo) — this is the documented setup step,
    since ticket create/update/delete requires being an agent/manager of the ticket's group.
  3. Created a ticket in that group through the UI (/app/ticketing/create), read it back,
    edited its title through the UI, then deleted it (POST .../1 with _method=DELETE
    spoofing) — all four steps worked correctly, ticket confirmed gone (404).
  4. Went to clean up the ephemeral group at /app/ticketing/settings (Groups tab) as the
    e2e spec's teardown does: POST ticketing.settings.groups.update with
    _method=DELETE spoofing (same pattern that worked for the ticket).

What I expected

The group delete to succeed (or at minimum, some way — UI button or endpoint — to
remove/archive a ticketing group), matching the comment in the existing e2e spec:
"the group DELETE shares its URI with groups.update (no separate destroy route name)".

What happened

  • The DELETE-spoofed request to ticketing.settings.groups.update returned 405
    Method Not Allowed
    (not a redirect/opaque-success like the equivalent ticket delete did).
  • Checked the live Ziggy route table (Object.keys(Ziggy.routes).filter(k => k.includes('ticketing') && k.includes('group'))) — only two routes exist:
    ticketing.settings.groups.store and ticketing.settings.groups.update. The update
    route's registered methods are ["PUT"] only — no DELETE, no separate destroy route.
  • Checked the Settings → Groups UI directly: the group row only has an "Edit" action
    (name/prefix/email/colour fields + Save/Cancel) — no delete/archive button anywhere.

Net result: there is no way, via the UI or any documented API endpoint, to delete or
archive a ticketing group once created.
This affects real usage (an admin who creates
a group by mistake, or retires a department, has no way to remove it) and also means the
dev-e2e suite's own ticketing flow test's teardown will fail once the e2e suite is running
again — every run leaves a permanently orphaned group behind in the shared dev DB.

I was unable to clean up my own test group [qa-sweep] Desk (id 1, prefix QAS2eeueo)
for this reason — it will remain in the dev DB.

Persona / URL

  • Persona: superadmin@playwright.test
  • URL: https://central-dev.vancs.org/app/ticketing/settings (Groups tab)
  • Route name: ticketing.settings.groups.update

Filed automatically by the Claude-driven QA sweep (/opt/qa-agent). The marker below dedupes repeat reports — do not edit it.

## What I did As part of a regression sweep of the ticketing app's CRUD flow (following `dashboard-e2e`'s own `tests/flows/ticketing/ticketing-crud.spec.js` pattern), I: 1. Logged in as `superadmin@playwright.test`. 2. Created an ephemeral ticketing group via `POST` to `ticketing.settings.groups.store` (name `[qa-sweep] Desk`, prefix `QAS2eeueo`) — this is the documented setup step, since ticket create/update/delete requires being an agent/manager of the ticket's group. 3. Created a ticket in that group through the UI (`/app/ticketing/create`), read it back, edited its title through the UI, then deleted it (`POST .../1` with `_method=DELETE` spoofing) — all four steps worked correctly, ticket confirmed gone (404). 4. Went to clean up the ephemeral group at `/app/ticketing/settings` (Groups tab) as the e2e spec's teardown does: `POST ticketing.settings.groups.update` with `_method=DELETE` spoofing (same pattern that worked for the ticket). ## What I expected The group delete to succeed (or at minimum, some way — UI button or endpoint — to remove/archive a ticketing group), matching the comment in the existing e2e spec: "the group DELETE shares its URI with groups.update (no separate destroy route name)". ## What happened - The DELETE-spoofed request to `ticketing.settings.groups.update` returned **405 Method Not Allowed** (not a redirect/opaque-success like the equivalent ticket delete did). - Checked the live Ziggy route table (`Object.keys(Ziggy.routes).filter(k => k.includes('ticketing') && k.includes('group'))`) — only two routes exist: `ticketing.settings.groups.store` and `ticketing.settings.groups.update`. The `update` route's registered methods are `["PUT"]` only — no `DELETE`, no separate destroy route. - Checked the Settings → Groups UI directly: the group row only has an "Edit" action (name/prefix/email/colour fields + Save/Cancel) — no delete/archive button anywhere. **Net result: there is no way, via the UI or any documented API endpoint, to delete or archive a ticketing group once created.** This affects real usage (an admin who creates a group by mistake, or retires a department, has no way to remove it) and also means the dev-e2e suite's own ticketing flow test's teardown will fail once the e2e suite is running again — every run leaves a permanently orphaned group behind in the shared dev DB. I was unable to clean up my own test group `[qa-sweep] Desk` (id 1, prefix `QAS2eeueo`) for this reason — it will remain in the dev DB. ## Persona / URL - Persona: `superadmin@playwright.test` - URL: `https://central-dev.vancs.org/app/ticketing/settings` (Groups tab) - Route name: `ticketing.settings.groups.update` --- _Filed automatically by the Claude-driven QA sweep (`/opt/qa-agent`). The marker below dedupes repeat reports — do not edit it._ <!-- claude-qa-fingerprint: 84238986e24a -->
jwed added the e2e-failure label 2026-07-26 15:59:27 -07:00
Author
Owner

Fixed in dashboard-ticketing 1175c20 (+ shell c3e4ecf).

There genuinely was no destroy route — only settings.groups.store and .update — so your teardown could never work and every sweep left a group behind. It also deleted the pre-existing [qa-sweep] Desk litter group in the process.

Deleting unconditionally would have been wrong: tickets.group_id is a RESTRICT FK and tickets are the audit record of real correspondence. So destroyGroup counts tickets and picks:

  • 0 tickets → real delete (agent access / projects / email connections cascade, nothing dangles). This is your teardown and the undo-a-typo path.
  • ≥1 ticket → archived (archived_at), tickets untouched and still readable.

You are TOLD which happened rather than inferring it from a 302 — an expectsJson() caller gets {"outcome":"deleted"|"archived","ticket_count":N}, and the UI confirm modal says which it will be before you click. Archiving is idempotent; settings.groups.restore undoes it.

Archived groups leave the pickers only. store() validation also rejects a stale archived id so the picker cannot be bypassed, and update() still allows a ticket's OWN group — otherwise a ticket stranded in an archived group would become uneditable. agentGroupIds()/isAgent()/isManager() are untouched, so existing agents keep working the remaining tickets.

Gating is byte-identical to updateGroup (permission:ticketing.settings + requireManagerAccess); a super admin with no manager row on that group is still refused, and that is asserted.

9 tests / 69 assertions; 10 mutations all detected — including forcing a hard delete on a group with tickets, which throws from the RESTRICT FK, so the database backs up the application check. Browser-verified: both modal wordings, the ARCHIVED badge and Restore, the group vanishing from the create picker and All-Tickets switcher while its ticket stays listed, and zero native confirm/alert dialogs.

Fixed in dashboard-ticketing 1175c20 (+ shell c3e4ecf). There genuinely was no destroy route — only `settings.groups.store` and `.update` — so your teardown could never work and every sweep left a group behind. It also deleted the pre-existing `[qa-sweep] Desk` litter group in the process. Deleting unconditionally would have been wrong: `tickets.group_id` is a RESTRICT FK and tickets are the audit record of real correspondence. So `destroyGroup` counts tickets and picks: - **0 tickets → real delete** (agent access / projects / email connections cascade, nothing dangles). This is your teardown and the undo-a-typo path. - **≥1 ticket → archived** (`archived_at`), tickets untouched and still readable. You are TOLD which happened rather than inferring it from a 302 — an `expectsJson()` caller gets `{"outcome":"deleted"|"archived","ticket_count":N}`, and the UI confirm modal says which it will be before you click. Archiving is idempotent; `settings.groups.restore` undoes it. Archived groups leave the pickers only. `store()` validation also rejects a stale archived id so the picker cannot be bypassed, and `update()` still allows a ticket's OWN group — otherwise a ticket stranded in an archived group would become uneditable. `agentGroupIds()`/`isAgent()`/`isManager()` are untouched, so existing agents keep working the remaining tickets. Gating is byte-identical to `updateGroup` (`permission:ticketing.settings` + `requireManagerAccess`); a super admin with no manager row on that group is still refused, and that is asserted. 9 tests / 69 assertions; 10 mutations all detected — including forcing a hard delete on a group with tickets, which throws from the RESTRICT FK, so the database backs up the application check. Browser-verified: both modal wordings, the ARCHIVED badge and Restore, the group vanishing from the create picker and All-Tickets switcher while its ticket stays listed, and zero native confirm/alert dialogs.
jwed closed this issue 2026-07-26 16:35:26 -07:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: jwed/central-ticketing#7