skip to content

A mobile app blocks its own use below a minimum supported version. How do you design cases proving the gate cannot be bypassed?

level: middleimportance: nice to knowfreq 30%

answer

  1. The blocking screen is advice, not enforcement
  2. Check exactly at the boundary version
  3. Try launching with the check unreachable
  4. Assert the service refuses the blocked version
  5. Cover the path after the update

basics

~20 s

Test the boundary and the ways around it: at the minimum version, one below it, and with the version check unreachable. The decisive case asserts the service refuses a blocked client, since a screen the app draws can always be skipped.

solid answer

~50 s

Split it into three families of cases. **Boundary.** Exercise exactly the minimum supported version, one release below it and one above, against a service configured with that minimum. An off-by-one in the comparison is the usual defect and it is invisible at any other version. **Bypass.** Try the paths a user reaches without meaning to: launching with the version check unreachable, resuming from the background past the blocking screen, opening a link that lands on a specific screen, and moving the device clock if the rule is date-based. Each answers one question — does the app fail open or closed? **Enforcement.** The client-side block is advisory. The case that matters sends a request declaring a blocked client version and asserts the service refuses it with a distinguishable code and records no side effect. Then cover the way out: after updating, the block must not reappear.

code

pseudocode · 16 lines
pseudocode
minimum_supported = 4.2.0

# boundary: one below, exactly at, one above
for client_version in [4.1.9, 4.2.0, 4.2.1]:
    launch(app, version = client_version)
    assert_blocked(client_version < minimum_supported)

# fail-open probe: the version source never answers
launch(app, version = 4.1.9, version_source = UNREACHABLE)
assert_blocked(true)        # a gate that opens when unsure is not a gate

# enforcement, independent of any screen
response = call_service(path = /orders,
                        declared_client_version = 4.1.9)
assert response.code == UPGRADE_REQUIRED
assert no_order_was_recorded()

go deeper

for a junior

Know what a minimum supported version is: below it the app refuses to work and asks the user to update. Be able to say why asserting that the blocking screen appears is not the whole test.

for a middle

Explain the mechanics — the comparison at exactly the minimum, one below and one above; behaviour when the version source cannot be reached; and why the service has to refuse the blocked build independently of any screen.

for a senior

Show the coverage you would insist on before a hard block ships: fail-open behaviour, resume-from-background and link entry paths, a blocked write that must record no side effect, and the recovery path after the update.

for a principal

Own the policy. Decide when a version is blocked outright rather than nudged, what that costs users who cannot update, and how a wrongly-set minimum is rolled back quickly once installs are already refusing to run.

## What a minimum-version gate is, and what it is not A minimum-version gate is a rule that an app build below some version may not be used. It usually appears as a blocking screen at launch: the app asks the service what the lowest supported version is, compares it against its own, and if it is behind it draws a screen offering an update and refuses to continue. The property that decides your case design is that **the app draws that screen itself**. It is the app deciding not to serve its own user. That makes it advice rather than enforcement: anything reaching a feature without passing through the screen — a resumed launch, a link that opens a specific screen, a request built by hand, an older build still installed — has not been stopped by anything at all. The gate is only as strong as the service behind it. So the coverage splits three ways: the comparison, the ways around the screen, and the enforcement that actually holds. ## Family one: the boundary The comparison is the part that is silently wrong most often, and it is wrong in exactly one place. - Run at **exactly** the minimum supported version and assert the app is usable. An off-by-one here blocks every user who did the right thing. - Run **one release below** and assert it is blocked. - Run **one release above** and assert it is usable. - Run a version whose numbering has an awkward shape — more segments, a suffix, a two-digit segment — and assert the comparison orders numbers rather than comparing text, where ten sorts before nine. Those four cases take minutes and catch a defect class that survives everything else, because every other case sits comfortably far from the line. ## Family two: the ways around it Each of these is a real entry path a user reaches without trying: - **The version check does not answer.** No connectivity, a slow response, an error. Does the app open or block? - **Resuming rather than launching.** The app was left in the background before the minimum was raised and is brought forward again. Is the check re-run? - **A link that opens a specific screen.** Does it land past the block? - **A date-based rule with the device clock moved.** If the gate is a deadline rather than a version, the device controls the input. | Behaviour when the check cannot answer | What it costs | When it is right | |---|---|---| | Fail open — allow use | An unsupported build reaches the service | Only where the service refuses it independently | | Fail closed — block use | A check outage looks like a total product outage | Where running an old build is genuinely unsafe | Most products fail open on the screen and closed at the service. Whichever the product chose, the case exists and asserts it deliberately rather than discovering it during an incident. ## Family three: the enforcement that holds This is the case that makes the others meaningful, and it never touches a screen. Send a request that declares a blocked client version and assert three things: 1. The service refuses it. 2. The refusal is **distinguishable** — a code the client can only read as update-required, never a generic authorization failure that the app will present as a sign-in problem. 3. **No side effect was recorded.** A blocked write must not land. A refusal that still writes is worse than having no gate, because the product now believes it is protected. ## The path out Coverage stops at the block far too often. The user updates — now what? Assert that the app opens normally with no residue of the blocking screen, that a task interrupted by the block can be started again, and that the refusal is not cached in a way that outlives the update. A gate that blocks correctly and then will not let go is the same outage with an extra step. ## What weak coverage looks like - A single case, far below the minimum, asserting that a screen appears. - No case at exactly the minimum version. - No case where the version source is unreachable. - No request-level case at all, so the entire gate rests on a screen the app draws for itself.

  • Should the app fail open or fail closed when the version check itself is unreachable?
    Decide it deliberately and test whichever you chose. Failing open keeps offline users working but lets a genuinely blocked build reach the service, so the service-side refusal has to be the real gate. Failing closed makes a check outage look like a total outage. Most products fail open on the screen and closed at the service, which is why both cases have to exist.
  • What do you assert about the request a blocked build sends anyway?
    That the service answers with a distinct, machine-readable refusal the client can only interpret as update-required, rather than a generic authorization error or a success with empty data. Assert the code rather than the wording, so a copy change does not break the case, and assert that nothing was written: a blocked write must record no side effect.

saying these in an interview costs you the question

  • Asserts the blocking screen appears and stops there
  • Assumes a screen the app draws cannot be skipped
  • Tests only versions far below the minimum, never the boundary
  • Lets the block open whenever the version check fails
  • Never checks the app works again after the update