How to Test Two-Factor Authentication on Flutter (Complete Guide)
How to Test Two-Factor Authentication on Flutter (Complete Guide) starts with understanding why 2FA is critical for Flutter apps and what typically goes wrong in production. Two‑factor authentication
How to Test Two-Factor Authentication on Flutter (Complete Guide) starts with understanding why 2FA is critical for Flutter apps and what typically goes wrong in production. Two‑factor authentication adds a second verification step—usually a time‑based one‑time password (TOTP), SMS code, push notification, or biometric factor—on top of a password. In Flutter applications the authentication flow often lives across multiple widgets, platform channels, and third‑party SDKs (Firebase Auth, Auth0, custom OAuth). A missed edge case can let an attacker bypass the second factor, lock out legitimate users, or expose secrets in logs. This guide walks you through a complete test strategy: a detailed test matrix, manual steps, automated unit/widget/integration tests, provider‑specific checks, accessibility and security considerations, and how autonomous, persona‑driven exploration surfaces bugs that scripted tests miss.
---
How to Test Two-Factor Authentication on Flutter (Complete Guide): Why 2FA Matters and Common Failure Modes
Why 2FA Is Non‑Optional for Flutter Apps
Flutter apps frequently handle sensitive data—personal health records, financial transactions, or enterprise credentials. Even if the same threat model that applies to native Android/iOS apps applies here: credential stuffing, phishing, SIM‑swap, and man‑in‑the‑middle attacks. A correctly implemented 2FA flow reduces the success rate of credential‑based attacks from ~80 % to <5 % according to recent breach analyses.
Typical Production Failures
| Failure Category | Symptom in Flutter UI | Root Cause (Flutter‑specific) |
|---|---|---|
| State loss during code entry | After entering the SMS code, the app returns to the login screen without error | The AuthBloc disposes before the asynchronous SMS verification callback resolves; the UI rebuilds with stale state |
| Incorrect handling of expired TOTP | User sees “Invalid code” despite correct entry, then gets locked out after three attempts | The TOTP validation uses DateTime.now() from the UI thread, which can drift if the isolate is paused during a heavy animation |
| Missing error propagation from platform channel | No toast or dialog appears when the native SMS retriever times out | The Flutter side only listens for a success channel; failure channel is never subscribed to |
| Accessibility label missing | TalkBack reads “button” instead of “Enter verification code” | The TextFormField lacks a labelText or semanticLabel, causing screen‑reader users to miss the field |
| Hard‑coded backup codes in assets | Backup codes visible when inspecting the APK | Developers placed static backup codes in assets/backup_codes.json for convenience, exposing them to anyone who unpacks the app |
| Rate‑limit bypass via hot reload | During dev, rapid hot reload lets a tester submit 20 codes in a second without being throttled | The rate‑limit logic lives in a Singleton that is re‑initialized on each hot reload, resetting the counter |
Each of these issues can slip through unit tests because they involve timing, platform interactions, or UI state that only manifests in a full‑stack run.
---
How to Test Two-Factor Authentication on Flutter (Complete Guide): Core Concepts and Flutter‑Specific Considerations
Authentication Architecture in Flutter
Most Flutter 2FA implementations follow one of three patterns:
- Bloc/Cubit with Repository – UI widgets dispatch events (
LoginStarted,OtpSubmitted) to a bloc that calls a repository exposingverifyPhoneNumber,verifyTotp, etc. - Provider/ChangeNotifier – A
AuthProviderholdsisLoading,errorMessage, anduserstate; widgets consume viaConsumer. - Direct SDK Calls – Widgets call Firebase Auth methods directly (
FirebaseAuth.instance.signInWithCredential(phoneAuthCredential)) and manage state withsetState.
Regardless of pattern, the testable surface includes:
- Input widgets (
TextFormFieldfor phone number, OTP fields) - Async boundaries (
Future,Stream,Completer) that bridge Flutter to native code (SMS retriever, biometric prompt) - State persistence (SecureStorage, Hive, SharedPreferences) that may retain OTP secrets across app restarts
- Platform channels (MethodChannel, EventChannel) used by plugins like
firebase_auth,flutter_secure_storage,local_auth
Testability Hooks to Build In
- Dependency injection – Provide an abstract
AuthRepositoryso tests can inject a mock or fake implementation. - Observable streams – Expose a
Streamthat test harnesses can listen to for state transitions. - Factory constructors for widgets – Allow passing a
bool forceErrorflag to simulate network failure or expired token without hitting the real backend. - Logging abstraction – Use a logger interface (
Logger) that can be swapped for aTestLoggercapturing emitted messages for assertion.
When these hooks exist, unit and widget tests can drive the 2FA flow without needing a real SMS gateway or biometric hardware.
---
How to Test Two-Factor Authentication on Flutter (Complete Guide): Test Matrix (Happy Path, Error Paths, Edge Cases, Accessibility, Security)
Below is a comprehensive matrix you can copy into a test‑management tool (e.g., TestRail, Zephyr) or a simple spreadsheet. Each row represents a distinct test scenario; columns indicate the test type (manual, automated unit/widget, integration, autonomous) and the expected verdict.
| ID | Category | Description | Test Type(s) | Expected Result |
|---|---|---|---|---|
| 1 | Happy Path – Phone + SMS | User enters valid phone, receives SMS, submits correct 6‑digit code, gains access | Manual, Widget, Integration | Login succeeds, navigation to home screen |
| 2 | Happy Path – Email + TOTP | User enters email, receives authenticator‑app secret, scans QR, enters correct TOTP, login succeeds | Manual, Widget, Integration | Same as #1 |
| 3 | Happy Path – Push Notification | User approves login via push (simulated via mock server) | Manual, Integration | Login succeeds |
| 4 | Error – Wrong OTP | User submits incorrect OTP three times | Manual, Widget | Shows “Invalid code”, remains on OTP screen, error count increments |
| 5 | Error – Expired OTP | User waits >30 s (TOTP) or >60 s (SMS) then submits code | Manual, Widget | Shows “Code expired”, allows resend |
| 6 | Error – Network Failure | Simulate loss of connectivity after OTP entry | Widget (mock repository) | Shows “Network error”, offers retry, does not crash |
| 7 | Error – Backend 500 | Mock server returns 500 after OTP verification | Widget, Integration | Shows generic error, logs details, no sensitive data leaked |
| 8 | Edge – Empty Phone Field | User taps submit with blank phone number | Widget | Shows validation error “Phone number required” |
| 9 | Edge – Non‑numeric Phone | User enters letters in phone field | Widget | Shows “Please enter digits only” |
| 10 | Edge – Leading Zero Truncation | Phone number starts with 0 (e.g., 01234…) and is stripped by formatting | Widget | Preserves leading zero after input mask |
| 11 | Edge – Max Length Paste | User pastes a 20‑character string into OTP field | Widget | Field accepts only first 6 characters, ignores rest |
| 12 | Edge – Biometric Fallback | Device lacks fingerprint; app falls back to OTP after biometric prompt times out | Manual, Integration | Shows OTP screen after timeout |
| 13 | Accessibility – TalkBack Labels | All input fields and buttons have meaningful semanticLabel | Manual (TalkBack), Widget (semantics test) | TalkBack reads “Enter phone number”, “Send code button”, etc. |
| 14 | Accessibility – Contrast | OTP field background vs. text meets WCAG AA (≥4.5:1) | Manual (contrast checker), Automated (flutter_lints) | Contrast ratio ≥4.5 |
| 15 | Accessibility – Touch Target | Buttons ≥48 dp | Manual (UI inspector), Widget (size test) | Touch target passes |
| 16 | Security – Rate Limiting | After 5 failed OTP attempts, further attempts are blocked for 5 min | Manual, Integration (mock backend) | Shows “Too many attempts”, timer counts down |
| 17 | Security – Code Reuse | Same OTP submitted twice is rejected on second attempt | Widget | Second submission shows “Code already used” |
| 18 | Security – Secret Storage | OTP seed or SMS token never written to logs or plain‑text files | Manual (logcat inspection), Automated (test logger) | No occurrence of seed in logs |
| 19 | Security – Memory Scrubbing | After successful login, OTP held in memory is cleared | Manual (memory profiler), Widget (unit test with fake OTP) | OTP string becomes null or overwritten |
| 20 | Edge – Locale Change Mid‑Flow | User switches device language after OTP screen appears | Manual translation updates without losing entered OTP | |
| 21 | Edge – Dark Mode | UI remains legible and contrast compliant in dark theme | Manual, Widget | All text meets contrast, icons adapt |
| 22 | Edge – Font Scale | System font size set to largest (200 %) | Manual, Widget | No overflow, scrollable if needed |
| 23 | Edge – Interruption (Call) | Incoming voice call arrives while OTP screen is visible; after call ends, app resumes correctly | Manual | OTP field retains entered digits, no crash |
| 24 | Edge – Battery Saver | Device in extreme battery‑saver mode; background SMS retriever may be delayed | Manual | App shows resend button after appropriate timeout, does not false‑positive “expired” |
| 25 | Autonomous Exploration | SUSA agent runs with curious, impatient, novice, adversarial, elderly, accessibility, power‑user personas | Autonomous (SUSA) | Discovers any of the above failures that scripted tests miss (e.g., hidden dead‑end after pressing back twice) |
How to use the matrix
- Manual – Execute on a physical device or emulator, noting observations.
- Widget/Unit – Write a test that pumps the widget, injects a fake repository, and asserts UI text or state changes.
- Integration – Launch the full app on an emulator or real device, drive it with
integration_test, and verify navigation or API calls. - Autonomous – Run the SUSA agent (see section “Autonomous, Persona‑Driven Exploration with SUSA”) and let it generate its own scenarios; compare its findings against the matrix to spot gaps.
---
How to Test Two-Factor Authentication on Flutter (Complete Guide): Manual Testing Step‑by‑Step
Preparing the Test Environment
- Device selection – Use at least one physical Android device (API 30+) and one iOS device (iOS 15+) to catch platform‑specific quirks.
- Clear app data – Before each test run, go to Settings → Apps → YourApp → Storage → Clear Cache and Data. This ensures a clean state (no leftover OTP secrets).
- Enable logging – Connect via
adb logcat(Android) orConsole.app(iOS) and filter for your app tag. - Set up mock SMS gateway – If testing SMS‑based 2FA, use a service like Twilio’s test credentials or a local mock server that returns a predefined code on request. Point your backend to this mock via environment variables or a
.envfile. - Configure accessibility tools – Turn on TalkBack (Android) or VoiceOver (iOS) and a contrast analyzer (e.g., Android’s Accessibility Scanner).
Test Script for Happy Path (SMS)
| Step | Action | Expected Observation |
|---|---|---|
| 1 | Launch app, navigate to Login screen | See “Enter phone number” field and “Continue” button |
| 2 | Enter a valid test phone number (e.g., +15551234567) | Keyboard shows numeric input; “Continue” becomes enabled |
| 3 | Tap Continue | Progress spinner appears; after 2‑3 s a toast says “We’ve sent a code” |
| 4 | Switch to SMS mock viewer, retrieve the 6‑digit code (e.g., 123456) | – |
| 5 | Return to app, enter the code in the OTP field (six separate boxes or single field) | Each box fills; after sixth digit, “Verify” button enables |
| 6 | Tap Verify | Spinner, then navigation to Home screen; no error toast appears |
| 7 | Verify session | Check that SecureStorage contains an auth token, and that the user profile shows the logged‑in email/ID |
| 8 | Log out and repeat steps 1‑7 with a different phone number | Same success flow, confirming no state leakage |
Test Script for Error Path (Wrong OTP)
| Step | Action | Expected Observation |
|---|---|---|
| 1‑3 | Same as happy path up to receiving code | – |
| 4 | Enter an incorrect code (e.g., 654321) | After sixth digit, “Verify” button stays enabled (or becomes enabled depending on design) |
| 5 | Tap Verify | Error toast: “Invalid code. Please try again.” OTP field clears or retains first five digits per spec |
| 6 | Repeat steps 4‑5 two more times | After third failure, either show “Too many attempts” or lock the OTP field for a cooldown period |
| 7 | Wait out cooldown (if applicable) | OTP field becomes editable again, timer shows remaining time |
| 8 | Enter correct code | Login proceeds as in happy path |
Accessibility Checks (Manual)
- TalkBack Navigation – Swipe left/right; each focusable element should announce purpose (
"Phone number, edit text"). - Activation – Double‑tap to activate buttons; ensure no double‑tap is required to trigger a action that normally needs a single tap (indicates missing
onPressed). - Reading Order – Verify that focus moves logically from phone field → continue button → OTP field → verify button.
- Contrast – Use the built‑in contrast checker; report any element below 4.5:1.
- Scalable Text – Set system font size to largest; ensure no clipping and that scrollable areas appear if needed.
Security‑Focused Manual Checks
- Log Inspection – After submitting OTP, scan logcat for the string “otp”, “token”, or the actual code. No appearance should be found.
- Memory Dump – Use Android Studio’s Memory Profiler to take a heap snapshot right after OTP entry; search for the OTP string; it should not be present.
- Rate‑Limit Simulation – Use a proxy (Charles, mitmproxy) to throttle responses; after 5 failed attempts, confirm the backend returns
429 Too Many Requestsand the app shows a timed lockout. - Backup Code Exposure – Unzip the APK/IPA, search for any
.json,.txt, or.xmlfiles containing strings resembling backup codes; none should exist.
---
How to Test Two-Factor Authentication on Flutter (Complete Guide): Automated Unit and Widget Tests
Setting Up the Test Environment
Add the following to dev_dependencies in pubspec.yaml:
dev_dependencies:
flutter_test:
sdk: flutter
mockito: ^5.4.0 # or mocktail if you prefer
flutter_lints: ^3.0.0
Create a folder structure:
test/
├─ auth/
│ ├─ login_page_test.dart
│ ├─ otp_page_test.dart
│ └─ auth_bloc_test.dart
└─ fakes/
├─ fake_auth_repository.dart
└─ fake_sms_service.dart
Example: Widget Test for OTP Input
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:your_app/auth/otp_page.dart';
import 'package:your_app/auth/auth_repository.dart';
class MockAuthRepository extends Mock implements AuthRepository {}
void main() {
late MockAuthRepository repo;
setUp(() {
repo = MockAuthRepository();
when(() -> repo.verifyOtp(any())).thenAnswer((_) async => true);
});
testWidgets('OTP page shows error on wrong code', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Provider.value(
value: repo,
child: const OtpPage(),
),
),
);
// Enter wrong OTP
await tester.enterText(find.byKey(const Key('otpField')), '111111');
await tester.tap(find.text('Verify'));
await tester.pump(const Duration(seconds: 1));
expect(find.textContaining('Invalid code'), findsOneWidget);
verify(() -> repo.verifyOtp('111111')).called(1);
});
testWidgets('OTP page navigates home on correct code', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Provider.value(
value: repo,
child: const OtpPage(),
),
),
);
await tester.enterText(find.byKey(const Key('otpField')), '123456');
await tester.tap(find.text('Verify'));
await tester.pumpAndSettle();
expect(find.byType(HomePage), findsOneWidget);
verify(() -> repo.verifyOtp('123456')).called(1);
});
}
*Key points*:
- Use
Provider.valueor a similar DI method to inject the mock repository. pumpAndSettlewaits for all animations and async work to finish.- Assert on navigation (
HomePage) and on the mock’s call count.
Unit Test for Bloc Logic
If you use flutter_bloc, test the state transitions:
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:your_app/auth/login_bloc.dart';
import 'package:your_app/auth/auth_repository.dart';
class MockAuthRepository extends Mock implements AuthRepository {}
void main() {
late MockAuthRepository repo;
late LoginBloc bloc;
setUp(() {
repo = MockAuthRepository();
bloc = LoginBloc(repository: repo);
});
tearDown(() => bloc.close());
test('emits [loading, success] when otp is valid', () async {
when(() -> repo.verifyOtp(any())).thenAnswer((_) async => true);
final expected = [
LoginState.loading(),
LoginState.success(),
];
expectLater(
bloc.stream,
emitsInOrder(expected),
);
bloc.add(const OtpSubmitted(otp: '123456'));
});
test('emits [loading, error] when otp is invalid', () async {
when(() -> repo.verifyOtp(any())).thenAnswer((_) async => false);
final expected = [
LoginState.loading(),
LoginState.error(message: 'Invalid code'),
];
expectLater(
bloc.stream,
emitsInOrder(expected),
);
bloc.add(const OtpSubmitted(otp: '654321'));
});
}
These tests run in milliseconds on CI and give you fast feedback on business logic.
---
How to Test Two-Factor Authentication on Flutter (Complete Guide): Integration Tests with flutter_test and integration_test
Why Integration Tests?
Unit/widget tests verify isolated pieces; integration tests confirm that the whole Flutter‑to‑backend pipeline works, including platform channels, native SMS retriever, and secure storage. They are slower but essential for catching state‑loss bugs like the “disposes before callback resolves” scenario described earlier.
Adding integration_test
dev_dependencies:
integration_test:
sdk: flutter
Create integration_test/app_test.dart:
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:your_app/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('End-to-End 2FA flow', () {
testWidgets('login with SMS OTP succeeds', (tester) async {
app.main();
await tester.pumpAndSettle();
// 1. Enter phone
await tester.enterText(find.byKey(const Key('phoneField')), '+15551234567');
await tester.tap(find.text('Continue'));
await tester.pumpAndSettle();
// 2. Wait for simulated SMS arrival (mock server returns after 2s)
await tester.pump(const Duration(seconds: 3));
// 3. Enter OTP from mock
await tester.enterText(find.byKey(const Key('otpField')), '987654');
await tester.tap(find.text('Verify'));
// 4. Assert home screen reached
expect(find.byType(HomePage), findsOneWidget);
});
testWidgets('shows error after three wrong OTP attempts', (tester) async {
app.main();
await tester.pumpAndSettle();
await tester.enterText(find.byKey(const Key('phoneField')), '+15551234567');
await tester.tap(find.text('Continue'));
await tester.pumpAndSettle();
// Simulate three failures
for (int i = 0; i < 3; i++) {
await tester.enterText(find.byKey(const Key('otpField')), '000000');
await tester.tap(find.text('Verify'));
await tester.pumpAndSettle();
expect(find.textContaining('Invalid code'), findsOneWidget);
}
// After third, expect lockout message
expect(find.textContaining('Too many attempts'), findsOneWidget);
});
});
}
Run with:
flutter drive --target=integration_test/app_test.dart -d emulator-5554
Mocking Backend Services
For reliable CI, spin up a lightweight mock server (e.g., using mockeray or a simple Express app) that exposes endpoints:
POST /auth/request-sms→ returns{code: "123456"}after a configurable delayPOST /auth/verify-otp→ returns{success: true}if code matches stored value, else{success: false, attemptsLeft: n}
Set the app’s base URL via environment variable or --dart-define=API_BASE_URL=http://10.0.2.2:3000 (the Android emulator’s alias for host localhost).
Testing Platform Channels
If you rely on a plugin like firebase_auth, you can use the firebase_auth_mocks package to avoid real network calls while still exercising the plugin’s MethodChannel logic:
import 'package:firebase_auth_mocks/firebase_auth_mocks.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('FirebaseAuth mock verifies OTP', (tester) async {
final mockAuth = MockFirebaseAuth(
// pre‑setup a phone auth credential that will succeed
mockUser: MockUser(isAnonymous: false, uid: 'test-uid'),
);
await tester.pumpWidget(
Provider<FirebaseAuth>(
create: (_) => mockAuth,
child: const MaterialApp(
home: OtpPage(),
),
),
);
await tester.enterText(find.byKey(const Key('otpField')), '111111');
await tester.tap(find.text('Verify'));
await tester.pumpAndSettle();
// Expect navigation to home or a success snackbar
expect(find.byType(HomePage), findsOneWidget);
});
}
---
How to Test Two-Factor Authentication on Flutter (Complete Guide): Using Firebase Auth and Other Providers for 2FA
Firebase Auth Phone Flow
Firebase Auth abstracts the SMS retrieval via verifyPhoneNumber. The Flutter side receives three callbacks: codeSent, codeAutoRetrievalTimeout, and verificationCompleted. To test:
- Mock
FirebaseAuth.instanceusingmockitoorfirebase_auth_mocks. - Simulate
codeSentby calling the callback with a fake verification ID. - Provide a fabricated SMS code via
PhoneAuthCredential.
Example snippet:
final FirebaseAuth auth = MockFirebaseAuth();
when(() => auth.verifyPhoneNumber(
phoneNumber: any(named: 'phoneNumber'),
verificationCompleted: any(named: 'verificationCompleted'),
codeSent: any(named: 'codeSent'),
codeAutoRetrievalTimeout: any(named: 'codeAutoRetrievalTimeout'),
onFailed: any(named: 'onFailed'),
)).thenAnswer((_) async => {});
final completer = Completer<void>();
when(() => auth.verifyPhoneNumber(
phoneNumber: '+15551234567',
verificationCompleted: any,
codeSent: captureAny,
codeAutoRetrievalTimeout: any,
onFailed: any,
)).thenAnswer((invocation) {
final codeSent = invocation.positionalArgument<Function(String, int?)>('codeSent');
codeSent('fakeVerificationId', 0); // simulate code sent
return completer.future;
});
Then in the widget test, trigger the phone number submission, wait for the codeSent callback to fire, call signInWithCredential(PhoneAuthCredential(verificationId: 'fakeVerificationId', smsCode: '123456')), and assert navigation.
Auth0 and Custom OAuth Providers
When using Auth0’s MFA, the flow typically involves:
- Username/password login → receives a
mfa_requirederror with amfa_token. - Polling
/mfa/challengewith the token to push a notification to the Auth0 Guardian app or to show a TOTP prompt. - Submitting the OTP via
/mfa/totpendpoint.
To test:
- Mock the
/oauth/tokenendpoint to return{error: "mfa_required", mfa_token: "abc123"}. - Mock the MFA challenge endpoint to return a status (
pendingorsuccess). - Verify that the app displays the appropriate UI (spinner, “Approve login in Auth0 Guardian” message) and proceeds only after the mock returns success.
Testing with Magic Link or Email‑Based OTP
Some providers send a one‑time link to email. In tests:
- Intercept the outgoing email via a fake SMTP server (e.g.,
maildev). - Extract the token from the URL and feed it into the app via deep link handling (
uni_linksplugin). - Assert that the app extracts the token, calls the verification endpoint, and logs the user in.
---
How to Test Two-Factor Authentication on Flutter (Complete Guide): Accessibility and Localization Checks for 2FA Screens
Automated Accessibility Testing
Add the accessibility_test package to dev_dependencies and write a test that uses the SemanticsHandler to verify labels and traits:
import 'package:flutter_test/flutter_test.dart';
import 'package:accessibility_test/accessibility_test.dart';
import 'package:your_app/auth/otp_page.dart';
void main() {
testWidgets('OTP page has proper semantics', (tester) async {
await tester.pumpWidget(const MaterialApp(
home: OtpPage(),
));
final semantics = await tester.getSemantics();
final phoneField = semantics.firstWhere(
(node) => node.label == 'Phone number',
orElse: () => throw StateError('Phone number field missing semantics'),
);
expect(phoneField.traits, contains(SemanticTrait.textField));
final verifyButton = semantics.firstWhere(
(node) => node.label == 'Verify code',
orElse: () => throw StateError('Verify button missing semantics'),
);
expect(verifyButton.traits, contains(SemanticTrait.button));
});
}
Run this test on every PR to catch regressions early.
Localization (l10n) Verification
If you use Flutter’s intl package with ARB files, ensure that all strings shown during 2FA have translations. A simple test:
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:your_app/l10n/l10n.dart';
import 'package:your_app/auth/otp_page.dart';
void main() {
testWidgets('OTP page displays correct Spanish strings', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: L10n.localizationsDelegates,
supportedLocales: L10n.supportedLocales,
locale: const Locale('es'),
home: const OtpPage(),
),
);
expect(find.textContaining('Ingrese su número de teléfono'), findsOneWidget);
expect(find.textContaining('Verificar código'), findsOneWidget);
});
}
Add similar tests for each supported language (e.g., fr, zh, ar).
Touch Target and Contrast Automation
Use the flutter_lints rule avoid_print is not relevant, but you can add custom lint rules via custom_lint to enforce:
- Minimum touch target size (
minTouchTarget: 48.0) - Contrast ratio (
minContrast: 4.5)
Create a lint_rules.yaml and run flutter analyze --options lint_rules.yaml.
---
How to Test Two-Factor Authentication on Flutter (Complete Guide): Security and Privacy Testing (Rate Limiting, Phishing Resistance, etc.)
Rate‑Limit Verification
Backend – Ensure the authentication endpoint enforces per‑IP or per‑account limits (e.g., 5 attempts per 5 min).
Client‑Side – The app should
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