How to Test Account Deletion on Android (Complete Guide)
Account deletion is a high‑risk operation because it touches user data, persisted state, backend services, and often triggers cascading clean‑up actions. A bug in this flow can lead to data leakage, o
Why Account Deletion Testing Matters
Account deletion is a high‑risk operation because it touches user data, persisted state, backend services, and often triggers cascading clean‑up actions. A bug in this flow can lead to data leakage, orphaned resources, or a broken user experience that triggers churn and regulatory penalties.
The business impact
When a user initiates delete, expectations are clear: the account and all associated personal data disappear from the service. If the app only hides the UI but leaves data on the server, users lose trust and may file complaints under GDPR, CCPA, or similar statutes. Conversely, an overly aggressive delete that removes shared content (e.g., group chat history) can anger other users and increase support load.
Technical risks
Deletion typically involves several moving parts:
- Local cache clearing – SharedPreferences, SQLite rooms, Files in internal/external storage.
- Network calls – One or more REST/GraphQL endpoints that may require fresh auth tokens, handle throttling, or retry on failure.
- Server‑side clean‑up – Cascading deletes in relational databases, removal from search indexes, revocation of refresh tokens, and GDPR‑style data‑subject requests.
- Concurrency – Background sync workers, push receivers, or JobScheduler tasks that may still be running when the delete request is sent.
- Fallback UI – After a successful delete the app should navigate to a logged‑out state or a welcome screen; a misrouted navigation leaves the user staring at a stale screen.
Any failure in these steps can manifest as a crash, an ANR, a silent data leak, or a zombie account that continues to consume resources.
Regulatory considerations
Privacy laws require that a deletion request be honoured within a defined time frame (often 30 days) and that the controller can demonstrate erasure. Testers must verify that the app not only sends the request but also validates the server response, handles error codes (e.g., 429 Too Many Requests, 500 Internal Server Error), and provides clear feedback to the user.
---
Test Matrix for Account Deletion
Below is a comprehensive matrix that covers the dimensions you should exercise. Each row represents a test case; columns indicate the observable outcome, the expected result, and notes on automation feasibility.
| ID | Category | Description | Preconditions | Steps | Expected Result | Pass/Fail Criteria | Automation Notes |
|---|---|---|---|---|---|---|---|
| A1 | Happy path | User deletes account via settings | Logged in, network reachable, no pending sync | Open Settings → Account → Delete Account → Confirm → Wait for success toast | Account removed locally, server returns 200, app navigates to login/welcome screen | PASS if toast shown, navigation correct, no local data remains | Espresso/UI Automator can assert toast and navigation |
| A2 | Happy path with biometric | Same as A1 but requires fingerprint/Face ID to confirm | Biometric enrolled, policy requires biometric for delete | Same as A1, system prompts biometric, user authenticates | Delete proceeds after biometric success | PASS if biometric prompt appears and delete completes | Use UiDevice.fingerprintAuthenticate() in UI Automator |
| B1 | Error – network loss | Delete initiated, then airplane mode toggled | Logged in, Wi‑Fi on | Start delete, enable airplane mode before network call completes | App shows error toast, retains account, offers retry | PASS if error shown, no data cleared, retry button functional | Mock network with adb shell cmd connectivity airplane-mode on |
| B2 | Error – server 401 (token expired) | Auth token stale at moment of delete | Logged in, token expired, refresh token unavailable | Attempt delete | App shows session expired, redirects to login | PASS if user sent to login, no delete request sent | Intercept with MockWebServer, assert redirect |
| B3 | Error – server 429 (rate limit) | Delete request throttled | Logged in, server returns 429 with Retry-After header | Attempt delete | App displays retry after X seconds, does not clear data | PASS if retry UI shown, no local wipe | Enqueue 429 response in MockWebServer |
| C1 | Edge – race with sync | Background sync pushes new data while delete in flight | Sync interval < 5 s, delete triggers network call | Start delete, force sync via adb shell cmd jobscheduler run … | Delete either aborts with conflict error or waits for sync to finish then proceeds | PASS if app handles conflict gracefully (shows message, does not lose data) | Use JobScheduler API to trigger sync |
| C2 | Edge – multi‑account | User has two accounts, deletes secondary while primary stays active | Two accounts added, secondary selected in account picker | Open account switcher, select secondary, initiate delete | Secondary removed, primary remains logged in, UI reflects correct account | PASS if only secondary data cleared, primary untouched | Reset account state between runs with adb shell pm clear |
| C3 | Edge – low storage | Device storage < 10 MB when delete attempts to clear local DB | Fill storage via adb shell dd if=/dev/zero of=/data/local/tmp/junk bs=1M count=500 | Attempt delete | App shows insufficient storage error, does not corrupt DB | PASS if error shown, DB intact, no crash | Monitor logcat for SQLiteFullException |
| D1 | Accessibility – TalkBack | User navigates delete flow with TalkBack enabled | TalkBack on, focus order logical | Swipe to Settings → Account → Delete → Activate | Delete confirmation dialog announced, focus lands on Cancel then Confirm buttons | PASS if all controls labeled, focus order correct, announcement complete | Use accessibilityService.performAction(GLOBAL_ACTION_BACK) in UI Automator test |
| D2 | Accessibility – color contrast | Delete button must meet WCAG AA contrast | Device in high contrast mode or forced via developer options | Verify delete button contrast ratio ≥ 4.5:1 | Button passes contrast check | PASS if contrast tool reports compliance | Use Android’s View#getDrawingRect + pixel analysis in instrumentation test |
| E1 | Security – token leakage | Network traffic inspected for auth tokens in delete request | Enable HTTP logging (e.g., adb shell setprop log.tag.HttpURLConnection VERBOSE) | Perform delete | No auth token appears in URL query params or logs | PASS if token only in Authorization header | Use HttpLoggingInterceptor in automated test |
| E2 | Security – re‑authentication | Delete should require recent auth (e.g., within 5 min) | Auth older than 5 min, no biometric fallback | Attempt delete | App prompts for password/biometric before proceeding | PASS if re‑auth screen appears | Mock timestamp, assert challenge shown |
| E3 | Security – data remnants check | After delete, verify no personal data left in files or backups | Use run-as to inspect app’s data directory | Delete account, then run-as com.example.app ls -la /data/data/com.example.app/ | No files containing PII (emails, tokens, photos) | PASS if grep for patterns returns nothing | Script with adb shell run-as + grep -r |
| F1 | Flow – incomplete confirmation | User taps Delete but backs out before confirmation | Delete button leads to confirmation dialog | Tap Delete, then press back or outside dialog | Dialog dismissed, account unchanged | PASS if no toast, no network call, data unchanged | Assert that no DELETE request sent (MockWebServer) |
| F2 | Flow – delayed success | Server returns 202 Accepted, actual deletion asynchronous | Endpoint returns 202 with job ID | Initiate delete | App shows “Deletion in progress” indicator, polls status, finishes with success | PASS if indicator appears, polling stops, final state correct | Use CountingIdlingResource for Espresso to wait on polling |
| G1 | Localization – RTL languages | Delete flow works in right‑to‑left locales (e.g., Arabic) | Device locale set to ar‑EG | Open settings, navigate to delete | All labels mirrored, buttons aligned correctly | PASS if layout mirrors, no clipped text | Use UI Automator to assert layoutDirection = RIGHT_TO_LEFT |
| G2 | Localization – long strings | Language with lengthy delete confirmation (e.g., German) | Locale set to de‑DE | View confirmation dialog | Text fits, no truncation, buttons accessible | PASS if all text visible, no overlap | Screenshot comparison or Espresso view assertions |
How to read the matrix
- Categories group similar risks (happy path, error, edge, accessibility, security, flow, localization).
- Automation Notes give a quick sense of whether the case can be covered with unit/instrumentation tests, mocked servers, or ADB commands.
- For a full regression suite you would automate the majority of rows (A‑F) and manually spot‑check the localization and accessibility rows, though many of those can also be automated with UI Automator and accessibility‑service checks.
---
Manual Testing Approach
Even with strong automation, a manual exploratory pass catches nuances that scripts may miss—especially around timing, system dialogs, and device‑specific behaviors. Below is a step‑by‑step guide you can follow on a physical device or an emulator.
Preparing the test environment
- Device state – Start with a clean user data partition:
adb shell pm clearor use a factory image. - Account setup – Create at least two test accounts (primary and secondary) via the app’s signup flow. Use disposable emails or a test backend that allows instant account creation.
- Network control – Install a traffic interceptor (e.g., NetHunter, Charles Proxy on Wi‑Fi, or
adb reverseto a local MockWebServer) to enable status‑code injection and latency simulation. - Accessibility tools – Enable TalkBack, Switch Access, and Font Scaling to maximum in Settings → Accessibility.
- Logging – Set
adb logtag *:Vor usepidcatto capture verbose logs; filter by your app’s tag for quick inspection. - Battery & performance – Plug the device in, disable battery optimizations for the app, and optionally enable “Stay awake” while charging to avoid sleep‑related flakiness.
Step‑by‑step manual test flow
| Step | Action | Observation points |
|---|---|---|
| 1 | Launch app, log in with primary account. | Verify home screen shows user‑specific data (e.g., profile name). |
| 2 | Navigate to Settings → Account → Manage Account. | Ensure the account entry is tappable, label readable. |
| 3 | Tap Delete Account. | Confirmation dialog should appear; check that TalkBack reads the message and button labels. |
| 4 | (Optional) Change device locale to Arabic, repeat step 3. | Layout should mirror; no clipping. |
| 5 | Confirm deletion. | Observe network traffic: a DELETE (or POST) request to /account/delete with proper auth header. |
| 6 | Monitor UI feedback. | Success toast, spinner, or progress bar should appear; error states should show appropriate messages. |
| 7 | Wait for flow to finish. | App should navigate to login screen or welcome screen; no remnants of previous account visible. |
| 8 | Verify local data cleared. | Run adb shell run-as and inspect SharedPreferences, databases, and cache folders for any leftover files containing email, tokens, or user‑generated content. |
| 9 | Check server side (if you have access). | Confirm that the account record is marked deleted or removed; ensure no active sessions remain. |
| 10 | Perform negative tests. | Repeat steps 1‑9 while: airplane mode toggled mid‑request, token expired, low storage, TalkBack gestures, or a background sync job forced via adb shell cmd jobscheduler run …. |
| 11 | Clean up. | Log out, clear app data, and restore device settings (locale, accessibility, network proxy). |
Observables and verification points
- Toast/Snackbar text – Should match the string resource for success/error; verify via
adb shell dumpsys notificationor UI AutomatorUiObject.getText(). - Navigation – Use
adb shell dumpsys activity activities | grep mResumedActivityto confirm the resumed activity after deletion. - Network logs – Look for
DELETE /account/delete(or equivalent) with status 200/202/4xx/5xx. Ensure auth header present, no token in URL. - Database queries – After deletion, run
adb shell run-as– should return zero rows for the current user.sqlite3 /data/data/ /databases/ .db> "SELECT * FROM users;" - File remnants –
find /data/data/should return empty.-type f -exec grep -l "user@example.com" {} \; - Accessibility – With TalkBack enabled, swipe through the delete confirmation; each element should announce its purpose (e.g., “Delete account, button”).
Common pitfalls
- Assuming success from UI only – A toast may appear even if the network call failed; always verify backend state.
- Ignoring pending WorkManager jobs – A delete may succeed UI‑wise, but a later job could re‑create local cache from a stale backup.
- Overlooking account picker state – In multi‑account apps, the UI may still show the deleted account in the spinner until a manual refresh; test that the picker updates automatically.
- Misinterpreting 202 Accepted – Treat it as “in progress”, not final; missing polling leads to false passes.
- Forgetting to clear credential manager – If the app uses SmartLock or Credential Manager, tokens may persist there; check via
adb shell cmd credential-manager get.
---
Automated Testing on Android
Automation gives repeatability and speed for the bulk of the matrix. Below are the layers you should implement, with concrete snippets that you can copy into a Gradle‑based Android project.
Unit and integration tests (JUnit 5 + Mockito)
Test the ViewModel or UseCase that orchestrates the delete flow. Mock the repository and network layer to simulate success, error, and edge responses.
// AccountDeleteViewModelTest.kt
@OptIn(ExperimentalCoroutinesApi::class)
class AccountDeleteViewModelTest {
private lateinit var viewModel: AccountDeleteViewModel
private lateinit var repository: MockAccountRepository
private lateinit var testDispatcher: TestDispatcher
@Before
fun setUp() {
testDispatcher = StandardTestDispatcher()
Dispatchers.setMain(testDispatcher)
repository = mock()
viewModel = AccountDeleteViewModel(repository)
}
@After
fun tearDown() {
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
@Test
fun `delete success updates state and navigates`() = runTest {
// given
whens(repository.deleteAccount())
.thenReturn(Result.success(Unit))
// when
viewModel.deleteAccount()
// trigger UI reaction
viewModel.uiState.collectFirst { it } // assume UIState sealed class
// then
assertTrue(viewModel.uiState.value is UiState.Success)
verify(navController).navigate(R.id.action_to_login)
}
@Test
fun `delete network error shows error state`() = runTest {
whens(repository.deleteAccount())
.thenReturn(Result.error(IOException("timeout")))
viewModel.deleteAccount()
val state = viewModel.uiState.collectFirst { it }
assertTrue(state is UiState.Error)
assertEquals("Network timeout", (state as UiState.Error).message)
}
}
Why this matters – Unit tests guard the business logic against regressions when you refactor the repository or change threading models. They run in a fraction of a second and can be part of every PR.
UI tests with Espresso
Espresso excels at verifying UI state, toast messages, and navigation. Use IdlingResource to wait for background network calls if you use OkHttp’s IdlingResource or a custom one for WorkManager.
// AccountDeleteEspressoTest.kt
@RunWith(AndroidJUnit4::class)
class AccountDeleteEspressoTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Before
fun setUp() {
// login helper – uses test credentials from res/values/test_strings.xml
loginHelper.loginAs("testuser@example.com", "SecurePass123")
}
@Test
fun deleteAccount_showsSuccessToast_andNavigatesToLogin() {
// open settings
onView(withId(R.id.action_settings)).perform(click())
onView(withText(R.string.settings_account)).perform(click())
onView(withId(R.id.btn_delete_account)).perform(click())
// confirm dialog
onView(withText(R.string.dialog_confirm_delete)).check(matches(isDisplayed()))
onView(withId(R.id.button_confirm)).perform(click())
// IdlingResource for network (assuming OkHttpIdlingResource)
IdlingRegistry.getInstance().register(OkHttpIdlingResource.create("okhttp"))
// success toast
onView(withText(R.string.toast_account_deleted))
.inRoot(isToast())
.check(matches(isDisplayed()))
// navigation to login
intended(hasComponent(LoginActivity::class.java.name))
}
@After
fun tearDown() {
IdlingRegistry.getInstance().remove(OkHttpIdlingResource.create("okhttp"))
}
}
Notes
- Replace
loginHelperwith your own utility that usesonViewto fill email/password and click sign‑in. - The
isToast()matcher is a common custom matcher; you can find it in the Android testing samples.
UI tests with UI Automator
UI Automator is useful for cross‑app interactions (e.g., system dialogs, permission prompts) and for testing accessibility services.
// AccountDeleteUiAutomatorTest.java
@RunWith(AndroidJUnit4.class)
public class AccountDeleteUiAutomatorTest {
private UiDevice device;
@Before
public void setUp() {
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
// Ensure we start from a clean state
device.executeShellCommand("pm clear com.example.app");
launchApp();
loginTestUser();
}
@Test
public void testDeleteWithTalkBack() throws UiObjectNotFoundException {
// Enable TalkBack via settings (requires ADB permission)
device.executeShellCommand("settings put secure accessibility_enabled 1");
device.executeShellCommand("settings put secure enabled_accessibility_services com.google.android.marvin.talkback/com.google.android.marvin.talkback.TalkBackService");
// Navigate to delete
new UiObject(new UiSelector().description("Settings")).click();
new UiObject(new UiSelector().text("Account")).click();
new UiObject(new UiSelector().text("Delete Account")).click();
// Confirmation dialog
UiObject confirm = new UiObject(new UiSelector().text("Delete account?"));
assertTrue(confirm.waitForExists(2000));
new UiObject(new UiSelector().text("DELETE")).click();
// Verify toast via accessibility event
UiObject toast = new UiObject(new UiSelector().className("android.widget.Toast"));
assertTrue(toast.waitForExists(5000));
assertEquals("Account deleted", toast.getText());
}
private void launchApp() {
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.example.app", "com.example.app.MainActivity"));
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
getInstrumentation().startActivitySync(intent);
}
private void loginTestUser() {
// fill email/password, click sign in – omitted for brevity
}
}
When to use UI Automator
- System overlays (e.g., permission dialogs, credential picker).
- Testing with accessibility services enabled (TalkBack, Switch Access).
- Scenarios where you need to press hardware buttons (HOME, RECENT) or change system settings mid‑test.
Using ADB commands for state checks
Sometimes the fastest way to assert a condition is a shell command. Integrate these into your test teardown or as helper methods.
# Verify no SharedPreferences entry for auth token
adb shell run-as com.example.app grep -r "auth_token" /data/data/com.example.app/shared_prefs/
# Check that the account table is empty
adb shell run-as com.example.app sqlite3 /data/data/com.example.app/databases/app.db \
"SELECT COUNT(*) FROM accounts;"
# Ensure no leftover files in cache
adb shell run-as com.example.app find /data/cache/com.example.app -type f -name "*.tmp" | wc -l
You can wrap these in JUnit @After methods to fail the test if any unwanted residue is found.
Comparison of automation approaches
| Approach | Strengths | Weaknesses | Typical use‑case for deletion |
|---|---|---|---|
| Unit/JUnit + Mockito | Fast, deterministic, isolates logic | No UI or system interaction | Validate ViewModel/repository logic, error handling, state transitions |
| Espresso | Real UI thread, synchronized with app, good for toast/navigation | Limited to single app, struggles with system dialogs | Verify screen flows, button states, toast messages, navigation after delete |
| UI Automator | Can interact with system UI, accessibility services, multiple apps | Slower, more brittle, requires API 18+ | Test TalkBack, permission dialogs, multi‑account picker, system settings changes |
| ADB shell checks | Direct access to filesystem, databases, logs | Not integrated with test runner, requires manual parsing | Post‑action validation of data cleanup, token removal, file remnants |
A robust strategy combines all four: unit tests for logic, Espresso for happy‑path/UI, UI Automator for accessibility and system‑edge cases, and ADB assertions in test teardown to ensure no data remnants.
---
Autonomous, Persona‑Driven Exploration with SUSA
While scripted tests cover the matrix you anticipate, real users behave in unpredictable ways. An autonomous explorer‑‑SUSA (SUSATest) is an autonomous QA platform that, given an APK or a web URL, explores the app using a variety of user personas, each with a distinct behavior model (curious, impatient, novice, adversarial, elderly, accessibility‑focused, power‑user, etc.). It does not need pre‑written scripts; it discovers flows by interacting with UI elements, handling dialogs, scrolling, and typing based on the persona’s policy.
How SUSA works
- Ingestion – You upload the APK or point SUSA at a staging URL. The platform installs the app on a fleet of real or emulated devices.
- Persona engine – Each persona defines:
- Interaction speed (e.g., impatient users tap quickly, elderly users hold longer).
- Error tolerance (novices may mis‑tap, adversarial users try invalid inputs).
- Goal orientation (power‑users aim to exhaust settings; curious users wander randomly).
- Exploration loop – The agent selects a UI element according to the persona’s policy, performs the action, observes the result (new screen, toast, crash, ANR), and updates its internal graph of visited states.
- Learning – Dead ends (screens with no forward actions) are marked and avoided in later runs, making each session smarter.
- Reporting – After a run, SUSA outputs:
- Crash/ANR stack traces.
- Detected accessibility violations (WCAG contrast, missing content‑description).
- Security hints (e.g., clear‑text token in logs).
- Flow completion status for predefined journeys (login, signup, account deletion).
Persona profiles relevant to deletion
| Persona | Typical behavior that surfaces deletion bugs |
|---|---|
| Impatient | Rapidly taps the Delete button multiple times before the confirmation dialog appears, testing double‑tap handling and race conditions. |
| Novice | May long‑press the Delete icon expecting a tooltip, or miss the confirmation and hit back, revealing missing undo or unclear affordances. |
| Adversarial | Enters garbage text in any exposed fields (e.g., if the app asks for a reason before delete) or tries to invoke delete via accessibility service gestures to bypass UI guards. |
| Elderly | Uses larger font sizes and slower gestures; can expose layout clipping or touch‑target size issues on the confirmation dialog. |
| Accessibility | Relies on TalkBack or Switch Access; ensures every element is labeled and reachable, catching missing content‑description on the delete icon or poorly announced dialogs. |
| Power user | Opens the app, adds a second account, switches accounts, deletes the secondary while a background sync is active, probing multi‑account and concurrency bugs. |
Because SUSA does not follow a predetermined script, it can stumble upon combinations that a manual tester might never think to try—like deleting an account while the device is in battery‑saver mode, or while a system update dialog is overlaying the settings screen.
What SUSA can uncover that scripts miss
- Hidden entry points – Some apps expose account deletion via a long‑press on the avatar in the navigation drawer, not via the Settings menu. A script that only navigates through Settings would never see it.
- Dynamic UI changes – If the app shows a “Delete account” button only after the user has viewed a premium upsell screen, a static test that jumps straight to Settings will miss the conditional visibility.
- Interleaved system events – SUSA’s random interleaving can place an incoming call, low‑memory warning, or DoNotDisturb toggle mid‑delete flow, exposing how the app handles lifecycle interruptions.
- Persona‑specific timing – An impatient user may trigger two delete requests in quick succession; if the backend does not deduplicate, you could see a double‑delete error or a 429 response that the happy‑path test never exercised.
Example output (truncated)
[Persona: Impatient]
- Action: Tap Delete button (x3) within 400ms
- Observation: First tap shows confirmation dialog, second tap triggers a second DELETE request (server returned 429)
- Verdict: FAIL – Rate‑limit not handled client‑side; user sees spinner forever
[Persona: Accessibility]
- Action: Enable TalkBack, navigate to Delete account via explore-by-touch
- Observation: Delete button lacks content‑description, announced as "unlabeled button"
- Verdict: FAIL – WCAG 2.4.7 (Focus Visible) & 4.1.2 (Name, Role, Value) violation
[Persona: Power user]
- Action: Add second account, start sync, switch to secondary, delete while sync ongoing
- Observation: Delete request returns 500 Internal Server Error; local cache cleared, leaving orphaned files in /data/data/com.example.app/cache/
- Verdict: FAIL – Server error not retried, data leak risk
These findings would not appear in a scripted test that only follows the “Settings → Account → Delete” path with a single tap and a mocked 200 response.
Integrating SUSA into your CI
You can invoke the SUSA CLI as a step in your pipeline:
# .gitlab-ci.yml snippet
test_account_deletion:
image: python:3.11
script:
- pip install susatest-agent
- susatest-agent run \
--apk ./app/build/outputs/apk/debug/app-debug.apk \
--personas impatient,novice,adversarial,elderly,accessibility,poweruser \
--flows account_deletion \
--output junit.xml
artifacts:
reports:
junit: junit.xml
The JUnit report can be parsed by your CI system to gate merges on any new failure.
---
Production‑Only Edge Cases
Even with exhaustive lab testing, certain conditions only manifest in the wild. Below are the most common production‑only pitfalls for account deletion, along with detection strategies.
Background sync conflicts
Many apps use WorkManager or SyncAdapter to periodically pull data (e.g., messages, feed items). If a sync job starts *after* the delete request has been sent but *before* the server acknowledges completion, the job may re‑insert data that the UI just cleared, leading to a “zombie” account.
Detection
- Enable verbose WorkManager logging:
adb shell setprop log.tag.WorkManager VERBOSE. - In your test, force a sync to run at a precise moment using
adb shell cmd jobscheduler run. - Assert that after the delete flow, no new rows appear in the sync‑triggered tables.
Server‑side throttling and retry logic
Production services often enforce per‑user rate limits on destructive endpoints. A client that blindly retries on 5xx may hammer the endpoint and get blocked, while a client that gives up too early leaves the account in a half‑deleted state.
Detection
- Use a mock server that returns 429 with a
Retry-Afterheader and observe whether the app respects it, shows a countdown, and eventually succeeds. - Check logs for exponential backoff implementation.
Token expiration during flow
If the delete request takes longer than the access token’s lifetime (e.g., due to poor network), the server may respond with 401. Apps that silently refresh the token and retry without re‑prompting the user can violate the expectation that deletion requires recent authentication.
Detection
- Set a short token expiry in your test backend (e.g., 30 seconds).
- Initiate delete, then delay the response (using a proxy like
toxiproxyornetshaper). - Verify that the app either shows a re‑auth prompt or fails gracefully with a clear message.
Multi‑account scenarios
When a user has multiple accounts, deleting one should not affect the others. However, shared preferences or a global singleton (e.g., a Firebase instance) might inadvertently wipe tokens for all accounts.
Detection
- Create two accounts, log in to both (using the app’s account switcher).
- Delete the secondary account.
- Verify that the primary account’s token is still valid by attempting a protected API call (e.g., fetch profile).
Local data remnants in external storage
Some apps cache media (profile pictures, attachments) on external storage (/sdcard/Android/data/). Deleting the account may clear the internal DB but leave these files, creating a privacy leak.
Detection
- After delete, run:
adb shell ls -l /sdcard/Android/data/com.example.app/
adb shell find /sdcard/Android/data/com.example.app -type f -exec grep -l "user@example.com" {} \;
Interruption by system dialogs
Incoming calls, low‑battery warnings, or system update prompts can overlay the app while the delete confirmation is visible. If the app dismisses the dialog incorrectly or leaks the confirmation state, the user may unintentionally confirm or cancel the delete.
Detection
- Use
adb shell emulator console(for emulators) or a third‑party app like “Call Simulator” to generate an incoming call during the delete flow. - Observe whether the app returns to the correct state after the call ends (i.e., still showing the confirmation dialog or having reverted to the settings screen).
---
Checklist for Account Deletion
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free