skip to content

How do you test the path where a permission the application already holds is revoked mid-use?

level: seniorimportance: nice to knowfreq 24%

answer

  1. Grants can be taken back
  2. Read the decision at use, not at launch
  3. Two uses, one revocation between them
  4. The stale cached answer is the defect
  5. Restore the device state in cleanup

basics

~20 s

Take the permission away between two uses of the feature inside one case, then assert the second use lands on the degraded path rather than erroring or hanging. Products fail here by checking once at launch and caching the answer.

solid answer

~50 s

Shape the case as **use, revoke, use again**. Arrange the capability as granted, exercise the feature and assert it works, then change the stored decision to denied from outside the application, return to the same screen and assert the degraded path. That second half is the whole point: an application that reads the decision once at launch and caches it will keep acting as though it is still permitted, then fail at the moment it actually touches the capability — with an empty result, an error the person cannot interpret, or a step that never completes. Write the assertions against what is on screen, not against internal flags, and do not assume the process survived the revocation, because some platforms restart it and some do not. Restore the stored decision in cleanup so the next case starts from a known device.

code

pseudocode · 20 lines
pseudocode
case "position capability withdrawn between two uses":
  arrange:
    set_stored_decision(capability = "position", value = GRANTED)
    launch_app()
    open_nearby_screen()
    expect_visible(results_list)          # proves it worked before

  act:
    set_stored_decision(capability = "position", value = DENIED)
    return_to_app()                       # process may or may not have survived
    open_nearby_screen()

  assert:
    expect_visible(explanation_banner)
    expect_visible(manual_area_picker)
    expect_settled_within(3 seconds)
    expect_no_unhandled_error()

  cleanup:
    clear_stored_decision(capability = "position")

go deeper

for a junior

Know that a capability approved earlier can be withdrawn later from outside the application, possibly while it is running. Being able to say the application must cope rather than crash is enough at this level.

for a middle

Explain the mechanics: a decision cached at launch becomes stale, so the check belongs immediately before each use. Describe a case that uses the feature, withdraws the permission, and then uses the feature again.

for a senior

Show the production judgement — asserting on observable behaviour rather than in-memory continuity, keeping the case indifferent to whether the process survived, and restoring shared device state so later cases are not poisoned.

for a principal

Own the scope call: which capabilities justify a revocation case at all, how often it runs, and what standard degraded behaviour every capability-gated feature must offer so each team does not invent its own.

## Grants are not permanent, and applications are written as though they are A permission is a decision stored on the device, and it can change while your application is in the foreground. Someone opens the platform's own settings surface and turns a capability off; a device-management policy withdraws it; a family-controls rule expires. The application did nothing wrong and nothing in its own code ran, yet the answer it depends on is now different from the one it read. The bug this produces is nearly universal in first drafts: the capability is checked once, early — at launch, or when a screen is first constructed — and the result is kept in a field. Every later use trusts the field. When the stored decision changes underneath, the field is stale, so the application confidently starts work it is no longer allowed to do. What the person sees depends on how the platform refuses: an empty result set that looks like "no data", an error string written for developers, or a step that simply never finishes. ## The shape of the case The case has to contain the change, because a revocation that happens outside a case is an environment event nobody can assert on. 1. **Arrange** the stored decision as granted, and launch. 2. **Act once** — use the feature and assert it works. This half matters: without it, a later failure cannot be attributed to the revocation rather than to the feature never having worked. 3. **Revoke** the stored decision from outside the application, the same way a person would, or through whatever state-setting seam the harness offers. 4. **Act again** — return to the same screen and use the feature a second time. 5. **Assert** the degraded path: the fallback renders, the person is told what stopped working, the flow settles rather than hanging, and nothing crashes. 6. **Restore** the decision in cleanup, so the device is left as it was found. Step 4 is where the case must be deliberately vague about mechanism. Some platforms restart the application when a stored decision changes; others leave the running process alone and simply refuse the next call. Your case should pass on both, which means it asserts on what the screen shows after returning to the feature, never on in-memory continuity or on a particular lifecycle sequence having occurred. ## Check at use, not at launch The design lesson the case enforces is a small table: | Where the decision is read | Behaviour after revocation | What the case sees | |---|---|---| | Once at launch, cached in a field | Application still believes it is permitted | Empty results, a raw platform error, or a step that never completes | | Immediately before each use of the capability | Application detects the refusal and branches | The degraded path, an explanation, and a route to fix it | | Only from the platform's refusal itself | Application reacts, but usually too late and too crudely | A dialog nobody can act on, or a crash on an empty handle | The middle row is the target, and it is also why this case is worth its upkeep: it is a design check disguised as a behavioural one. A suite that contains it makes "read the decision at the point of use" a rule the code cannot quietly drift away from. ## What the product owes the person at that moment Not crashing is the floor, not the goal. Good behaviour on discovery of a withdrawn capability has four parts: - **Stop the work in flight** that depended on it, rather than letting it fail deeper in and surface as something unrelated. - **Say plainly what stopped working**, in the product's own words, at the place the person was looking. - **Offer the one action that fixes it** — a route to the platform's settings surface, or an alternative that reaches the same outcome without the capability. - **Keep the rest of the screen usable**, so a withdrawn capability degrades one feature rather than bricking a journey. Silently returning nothing is the worst of the outcomes, because it is indistinguishable from the product being broken and generates support contacts nobody can diagnose. ## Keeping the device clean Permission decisions are shared state on a device that later cases will also use. Restore the decision in a teardown that runs whether the case passed or failed, and — belt and braces — have every capability case set the decision it needs at the start rather than trusting teardown alone, so one crashed case cannot poison a whole run. Where a pool of devices is shared across teams, prefer a fresh installation or an explicit state reset over hand-repairing devices between runs; a device whose stored decisions nobody can predict produces failures that look intermittent and are not. ## How much of this to carry This is a differentiator rather than a gate. Carry a revocation case for the capabilities the product genuinely depends on and where the degraded path is non-trivial, and let the rest rely on the plain-denial cases. A revocation case per capability per screen is upkeep nobody will maintain.

  • How do you stop this case leaving the device in a state that breaks the next one?
    Restore the stored decision in a teardown that runs whether the case passed or failed, and have every capability case also set the decision it needs at the start, so a crashed case cannot poison its successors. On a shared pool of devices, prefer a fresh installation or an explicit state reset over hand-repairing devices between runs.
  • Should the case assert that the process restarted when the decision changed?
    No. Whether a running process survives a change to a stored decision differs between platforms and between releases, so asserting it makes the case a platform detector rather than a product check. Assert what the person sees after returning to the feature: the degraded path renders, it settles, and nothing is left half-finished.
  • What is the single most common defect this case catches?
    A capability check performed once at launch and cached in a field. Every later use trusts the stale value, so the application starts work it is no longer permitted to do and fails deep inside, where the failure surfaces as an empty result or an uninterpretable error rather than as a clean degraded path.

A building pass that worked this morning can be deactivated at lunchtime. Code that reads the pass once on arrival will walk cheerfully towards a room it is no longer allowed into, and only the lock will stop it.

saying these in an interview costs you the question

  • Reads the permission once at launch and caches it
  • Assumes an approved capability cannot be taken away
  • Dismisses the revoked path as an unreachable edge case
  • Leaves the device's stored decision changed after the case
  • Asserts internal cached flags rather than visible behaviour
  • Treats an empty result as acceptable degraded behaviour