26 roles and permissions test cases, checked at the server and not the button
Twenty-six test cases for roles and permissions — privilege escalation, horizontal access, the hidden button that still works, and what happens the moment a role changes.
- 26 cases
- 7 coverage types
- Last verified Sep 16, 2026
There is one mistake that accounts for most permission bugs, and it is not a missing check. It is a check that exists in the interface and nowhere else.
The button is hidden from a viewer, so the feature looks locked. The endpoint behind it is not, so anyone who knows the URL is an editor. Every test that drives the browser passes, because the browser never shows the tester the thing they are not supposed to click.
So most of these cases are written in pairs: once through the interface, once straight at the endpoint. If you only run the first half of each pair, you have tested the design, not the permission.
Showing 26 of 26 cases
| ID | Test case | Type | Priority | Preconditions | Steps | Test data | Expected result |
|---|---|---|---|---|---|---|---|
| ROLE-01 | Each role sees the pages it should | Functional | High | One account per role |
| admin, editor, viewer | Each role sees exactly the documented set, with no entry that errors when opened. |
| ROLE-02 | A new member gets the least privileged role by default | Functional | High | An open invitation |
| — | The account is created as the lowest role. Nothing is granted by omission. |
| ROLE-03 | Changing a role takes effect without a new sign-in | Functional | High | A signed-in editor |
| — | The action is refused. The permission is read per request, not cached from sign-in. |
| ROLE-04 | A promoted user gains access immediately | Functional | High | A signed-in viewer |
| — | The page opens without signing out and in again. |
| ROLE-05 | A custom role grants exactly what it lists | Functional | Medium | Custom roles are supported |
| one permission | The listed action succeeds and the adjacent ones are refused. |
| ROLE-06 | Permissions inherited from a group apply | Functional | Medium | Group-based access is supported |
| — | The group grant applies, and removing them from the group removes it again. |
| ROLE-07 | The last administrator cannot be removed | Functional | High | Exactly one admin remains |
| — | Refused with an explanation. An account with no administrator is unrecoverable. |
| ROLE-08 | A viewer cannot edit through the interface | Negative | High | Signed in as a viewer |
| — | The controls are absent or disabled, and no keyboard path reaches them. |
| ROLE-09 | A viewer cannot edit through the endpoint | Security | High | A viewer session and a proxy |
| the editor's PATCH | Refused with 403. This is the case that catches a check that lives only in the interface. |
| ROLE-10 | A direct URL to a restricted page is refused | Security | High | Signed in as a viewer |
| /settings/members | Refused or redirected. Not rendered with the data already loaded. |
| ROLE-11 | One user cannot read another user's record | Security | High | Two users with their own records |
| B's record id | Refused. Horizontal access is checked on ownership, not only on role. |
| ROLE-12 | A role field in the request body is ignored | Security | High | Signed in as a viewer |
| role=admin | The role is unchanged. The field is not bound from user input. |
| ROLE-13 | An invitation cannot grant more than the inviter holds | Security | High | Signed in as an editor |
| role=admin | Refused. Escalation by proxy is the same escalation. |
| ROLE-14 | Permissions are re-checked on every request, not just the first | Security | High | A signed-in admin |
| — | Refused. A valid session is not a standing grant. |
| ROLE-15 | An expired or revoked session cannot act | Security | High | An admin signed in on two devices |
| — | Refused immediately, not at the next natural expiry. |
| ROLE-16 | Elevated access is time-bound where documented | Security | Medium | Temporary elevation is supported |
| — | Access lapses on its own without an administrator having to remember. |
| ROLE-17 | Support impersonation is visible and logged | Security | High | Impersonation is supported |
| — | A persistent banner shows the impersonation, and the log records both identities on every action. |
| ROLE-18 | Every permission change is written to the audit trail | Data integrity | High | Audit logging is on |
| — | An entry names who changed what, for whom, and when. The entry cannot be edited or deleted from the interface. |
| ROLE-19 | Deleting a role does not orphan its members | Data integrity | High | A custom role with members |
| — | Either the deletion is refused while members remain, or they fall back to the lowest role. They never keep a permission that no longer exists. |
| ROLE-20 | Removing a member revokes their access everywhere | Data integrity | High | A member with an active session |
| — | Their session stops working, their invitations lapse, and their API tokens stop authenticating. |
| ROLE-21 | Ownership transfers completely | Data integrity | Medium | Two admins |
| — | B holds every owner-only permission and A holds none. There is no window where both or neither do. |
| ROLE-22 | A permission denial explains itself without leaking | Usability | Medium | Signed in as a viewer |
| — | The message says access is needed and who to ask, and does not describe the record that is being withheld. |
| ROLE-23 | The permission matrix is a real table | Accessibility | Medium | The role settings page |
| — | Header cells are associated with their rows and columns, so a cell is announced with the role and permission it belongs to. |
| ROLE-24 | Every permission control is keyboard operable | Accessibility | High | The role settings page |
| — | Every control is reachable and operable, and the result is announced. |
| ROLE-25 | Role checks do not slow the page | Performance | Low | An account in many groups |
| 50 groups | Within the documented budget. Permission resolution does not grow linearly with group count on every request. |
| ROLE-26 | Two admins editing the same role do not overwrite each other | Data integrity | Medium | Two admin sessions |
| — | The second save is either merged or refused with a conflict. It does not silently discard the first. |
Case IDs are positional within this set, not stable identifiers. Import the set, then let your own tool assign its IDs.
How to use this set
Build the fixture first. Every case here needs at least two accounts at different levels plus a record that neither of them owns, and half of them need a proxy or an HTTP client to send a request the interface would not let you build.
What we deliberately left out
Signing in, session handling and SSO are covered elsewhere. Field-level and row-level rules inside a single record get their own set, because those are usually expressed in the data layer rather than in the role.
Questions about this set
We only have two roles. Is this overkill?
No. The escalation and horizontal access cases apply to any product with more than one account, and those are the ones that matter.
How do we test the endpoint half without writing code?
Any proxy that lets you replay a captured request works. Sign in as the privileged role, capture the action, then replay it with the unprivileged session cookie or token.
Should the refusal be 403 or 404?
Either, as long as it is consistent. A 404 for records you do not own reveals less, but mixing the two lets someone tell the difference between "does not exist" and "not yours".
Somewhere to keep these once you have run them
Tesbo holds the cases, the runs and the results in one place, so the next release starts from what the last one proved.