Skip to content
DedicatedPHP Contact

How to Test Authorization in PHP Integration Tests

Turn access rules into integration scenarios that verify permissions, isolation between users, and the absence of unauthorized side effects.

Authorization test matrix mapping users, roles, resources, and allowed or denied outcomes

A person being able to log in does not prove that they are authorized to view or modify every piece of application data. Authentication identifies the user; authorization determines what they can do, which resource they can act on, and under what conditions. A route may require a valid session and still allow someone to read another account’s order if the relationship between the user and the resource is not checked.

PHP authorization tests should verify this boundary through the application’s entry points, such as an HTTP request to a web route or API. The goal is not to test how a framework implements its policies, but to confirm observable behavior: who can do what, when a request is rejected, and which data remains unchanged.

Translate access rules into a matrix

Translate access rules into a matrix — DedicatedPHP visual guide

Before writing tests, express the rules using four elements: actor, action, resource, and context. Context includes conditions that affect permission, such as belonging to the same organization, owning the record, or having a particular status. This structure prevents authorization from being reduced to a list of roles.

  • Actor: anonymous user, member, manager, or administrator.
  • Action: view, create, edit, delete, or approve.
  • Resource: an order, document, account, or another protected object.
  • Context: owner, organization, resource status, or current relationship.

For example, a rule might allow a member to view their own orders, while a manager can view orders belonging to their organization. An administrator might have broader access, subject to the product’s actual rules. The matrix turns each business statement into testable cases and makes missing combinations visible.

You do not need to test every imaginable permutation. Prioritize trust boundaries: a user without a session, two users in the same organization, users in different organizations, a person with a privileged role, and a resource that does not belong to them. Add special contexts that change the decision, such as an archived order if its permissions differ from those of an active one.

Prepare representative integration scenarios

Set up data explicitly and keep it small. A typical test might create two organizations, one user in each, and a resource associated with one of them. Then authenticate as the user attempting access and send a request to the application’s public route using the resource identifier. This exercises the entry point, authentication, authorization, and response together.

For each important rule, include at least one allowed case and one denied case. If the owner can edit a resource, verify that the authorized edit works and that another user cannot do the same. A positive case is essential: a suite that only expects rejections could pass even if the application also blocked users who do have permission.

Also check for a nonexistent resource. The response to an unknown identifier may differ from the response to an existing resource the user is not allowed to access. Some applications use a not-found response to avoid revealing that the resource exists; others indicate that access is forbidden. The test should reflect the product’s deliberate policy, not impose a universal convention.

Use factories, fixture builders, or test helpers that produce valid, readable states. Avoid relying on fixed identifiers, execution order, or shared data that another test could modify. If persistence is part of the behavior you want to verify, check the result in the database using the project’s usual tools; do not assume that a successful response alone guarantees the correct state.

Test isolation between users and organizations

Isolation deserves dedicated cases because errors often occur when an identifier in the URL or request body is changed. Create a resource for one account and try to view, edit, or delete it using another identity. Repeat the check across organizations when the application scopes data by company. For critical operations, test each action: protecting a read does not prove that downloads, exports, or updates are protected as well.

To avoid duplicating the entire suite, share common setup and parameterize only the dimensions that express the rule. For example, a list of actor-resource pairs can define which combinations are allowed. Keep case names specific: “member of another organization cannot edit the order” is more informative than an opaque set of Boolean values. Separate scenarios when the method, route, or expected effects differ.

Avoid testing every combination of roles and resources when many do not represent distinct rules. Instead, identify justified equivalences and retain explicit cases for exceptions and boundaries. If roles are hierarchical, do not assume that one automatically inherits all the permissions of another: verify the effective rule defined by the product.

Verify responses and side effects

When a request is denied, check both the response and that the protected operation was not executed. Depending on the interface, the response might be a redirect, an unauthorized or forbidden status, or a not-found response. Check the relevant contract—status, format, and message when it matters—without coupling the test to unnecessary presentation details.

For a denied update, verify that sensitive fields remain unchanged. For a deletion, verify that the record is still available. For an operation that generates an invoice, notification, or event, check that this effect did not occur either. Assertions should cover the effects that matter to the business, not just the HTTP status code.

An unauthorized request must not allow partial changes either. If the flow performs several operations, verify that the rejection happens before any mutations or that the transaction leaves the system in a consistent state. You do not need to inspect private methods to prove this: observe the response and the persisted data or effects.

Keep tests resilient to implementation changes

Integration tests should go through a stable application interface, such as a route and a request with an authenticated identity using the available test mechanisms. Avoid calling an internal policy class directly if you want to validate actual access to a resource: doing so could leave the route registration, middleware, or object-loading behavior untested.

At the same time, do not turn the suite into a replica of the entire application. Test the authorization contract at representative points and reserve unit tests for pure rules that need many context cases. The combination helps pinpoint failures: a rule test can isolate a condition, while the integration test confirms that the rule protects the exposed flow.

When roles, routes, or policies change, review the matrix before changing expectations. A test updated only to accept the new result can hide an accidental expansion of permissions. Record why the rule changed, identify which actors gain or lose access, and add cases that protect the new boundaries.

Checklist for reviewing a change

Checklist for reviewing a change — DedicatedPHP visual guide
  • Are the actor, action, resource, and context defined?
  • Is there at least one allowed and one denied scenario for the affected rule?
  • Is access between users or data scopes tested where applicable?
  • Are nonexistent resources and the chosen policy for avoiding information disclosure covered?
  • Does the request go through the entry point that should be protected?
  • Is it verified that a denial does not change data or generate side effects?
  • Is test data isolated, readable, and reproducible?
  • Do the expectations reflect a product decision rather than an incidental framework detail?

A useful suite does not prove that “permissions exist” in the abstract: it proves that relevant combinations work and that unauthorized ones do not cross the boundary. Keeping this matrix alongside concrete integration tests makes it easier to review access changes without tying security to a specific internal implementation.

Want to apply these ideas to your project?Let’s discuss your PHP platform.
View related service