A city facilities team is rolling out a booking API before its public interface is ready. Residents can view their own reservations; staff can move bookings within their assigned facility; finance can review fees without changing reservations. The release question is whether those rules hold at the API itself, including when clients ignore every restriction in the interface. The examples below use a disposable deployment and synthetic accounts. Record the approved hosts, dates, accounts, request limits and allowed write operations before enabling any active testing. Give a named operator the ability to stop the run and restore the fixture database.
Check the repository and build before touching the API
Start with the release commit and produce an inventory of what actually ships. Include server code, migration and build jobs, example environment files, container build instructions, and any generated client that might embed a backend credential. Run secret detection against changed lines for fast feedback and against history in a separate job; removing a key from today's checkout does not remove it from prior commits or published artifacts. Report the location, secret type and masked fingerprint. If a credential might be live, have its owner revoke or rotate it, then check access records for the period of exposure. Never validate a suspected key by trying it against a third party service. Confirm the replacement works using the owning team's approved check and confirm the old credential can no longer authenticate.
Compare the resolved dependency list with security advisories. An alert for a build utility that never ships needs different evidence from an alert in the request parser. Record the version actually installed, the affected configuration and whether user input can reach the vulnerable code. An exception needs an owner, a reason and an expiration date. Rebuild after updating the dependency and inspect the new inventory so a stale image or transitive copy does not quietly carry the old version into release.
Trace a few representative inputs manually. In the booking search endpoint, query parameters must reach the data layer as values rather than query fragments. If an integration allows a callback URL, check destination policy and redirect handling before the server makes a request. Review which values are returned and which end up in logs, including error paths. Turn each finding into an entry with a file or build reference, impact, owner and a reproduction that does not contain secret material. A scanner's rule match is a lead; evidence that the code path can be reached is what helps prioritize it.
Test permission boundaries with more than one identity
Draw a matrix of identity, operation, resource owner and facility. Seed two residents, two facilities and a staff account for each facility. First prove that each resident can retrieve their own reservation and a staff member can update one in their assigned facility. Capture those positive results as controls. Then send an otherwise identical read using the other resident's reservation ID. A denial should not reveal the reservation details, its fees or any data about its owner. Apply the same boundary to lists, exports and batch lookups: one protected object in a collection is enough to fail the case. A single successful check on the detail route says nothing about the list route's filters.
For a write check, ask a resident to change their own permitted contact field and include a synthetic staff-only field, such as a fee waiver. According to the documented contract, the server should reject the request or ignore that field. Read the record as a permitted observer and compare the before and after state. Check scheduled jobs and notifications too: a denial response can still hide a write or an event sent before authorization. Repeat this approach for a staff member outside their facility and for finance attempting an update. Keep fixture resets separate from the requests under test; otherwise a reset can mask the change you are trying to catch.
Exercise authentication independently of authorization. Compare a valid token with missing, expired and wrong-audience tokens that you obtained or minted within the test environment. A valid token should still fail where the role or scope is insufficient. For rate limits, use a dedicated account, a bounded request budget and a single known route; confirm expected limit behavior and recovery without flooding shared infrastructure. For business operations like cancelling a booking, test whether the transition remains prohibited once a room has been checked in, even if the client sends the request directly.
Map checks to business actions before adapting them to different protocols. REST can expose the resource ID in a URL, GraphQL can pass it to a resolver or alias several lookups, and SOAP or RPC can encode it inside a method payload. Test the boundary at each entry point you actually support. For a read, assert that protected data is absent from the whole response, including partial GraphQL results. For a write, verify the stored state even when an RPC envelope reports an error. Preserve the request ID and method name so the service owner can locate the correct authorization decision in the logs.
Build a small automated suite and earn broader coverage
Start from the version of OpenAPI or the request collection that corresponds to the deployed commit. Generate functional checks for shape, status and required fields. Write the identity and ownership cases explicitly; a schema usually has no way to say that one resident may read reservation A but not reservation B. Use a fixture manifest that records account labels and resource IDs with no credentials. Take tokens from the runner's configured secret store and prevent the reporter from attaching headers or unredacted response payloads. If the contract and deployment disagree about available methods, resolve that mismatch before interpreting a pass.
For example, a merge request alters the filter used by the bookings export. Seed one reservation per resident and request the export as resident A. Assert that the exported row set is exactly A's set and then use an authorized staff identity to confirm that the second fixture was present. Run that deterministic test on every change to the export or authorization layer. Keep a broader, scheduled suite for bounded input variation, negative transitions and cross-protocol parity. For a larger authorized assessment, compare discovered endpoints to the approved inventory and route scope discrepancies back to the owner before testing them. Never let discovery expand the approved host list or enable mutation automatically.
Run static checks on every change and run security cases that need credentials against an isolated deployment. Separate permission failures from setup failures: a run where the test identities could not sign in is inconclusive even if every protected request returned a denial. For blocking cases, publish the failing check ID, expected boundary, actual result, redacted request ID and whether any side effect persisted. Keep an explicit list of skipped checks and why. Assign unresolved issues to owners and rerun the same case after the fix, including a positive control, before closing the finding.
Choose tooling by what it can prove
Use a repository scanner to locate potential leaks and risky dependencies, and a review assistant to trace whether an input reaches a sensitive sink. Give either one a restricted repository copy and decide in advance whether it may upload snippets. Use contract tooling to scaffold repeatable cases if its output can be inspected before execution. A service scanner that can accept two identities may assist with route coverage, but its report cannot establish ownership rules unless you supplied those rules and fixture pairs. Disable active discovery and write probes by default and review any generated requests for host, method and side effects. Keep the manual role matrix as the authority for expected behavior. You can run the whole baseline with existing CI, a request client and a test database; adding a scanner makes sense only when it gives evidence you currently miss.
Troubleshooting and FAQ
**Every negative request returns unauthorized. Did we pass?** Check whether a normal request works for the same identity. A token with the wrong audience, a proxy dropping the authorization header or an expired fixture can all make a suite falsely pass. Fix the setup and rerun the negative case with its corresponding positive control.
**The API returned success without disclosing another reservation. Is that acceptable?** It depends on the contract. An empty filtered list can be correct. A successful write that modified someone else's data is a failure even when its response has no body. Measure the expected state as well as the response.
**Can we start before the interface exists?** Yes. Agree on the contract, role model and seeded fixtures with the service owner. Use direct calls to verify behavior, then keep a small set of interface tests to make sure the client handles denial and recovery correctly.
**How does this differ from ordinary functional testing?** Functional checks exercise permitted behavior. Security checks hold the operation constant and change an assumption, such as the caller, owner, scope or input. Pair the two so a broken endpoint cannot masquerade as a working defense.
**What if a run changes unexpected data?** Stop the runner and record the affected fixture IDs and request IDs. Ask the owner to check persisted state and queued events before resetting the environment. A fresh test starts from a known state with the suspect case disabled until the cause is understood.