diff --git a/mobile/apps/auth/.gitignore b/mobile/apps/auth/.gitignore index c13b96038b..0f55adfcc5 100644 --- a/mobile/apps/auth/.gitignore +++ b/mobile/apps/auth/.gitignore @@ -33,6 +33,8 @@ .pub/ /build/ macos/build/ +.gradle/ +settings.local.json # Web related lib/generated_plugin_registrant.dart diff --git a/mobile/apps/auth/integration_test/auth_flow_test.dart b/mobile/apps/auth/integration_test/auth_flow_test.dart new file mode 100644 index 0000000000..794f305a30 --- /dev/null +++ b/mobile/apps/auth/integration_test/auth_flow_test.dart @@ -0,0 +1,254 @@ +import 'package:ente_auth/app/view/app.dart'; +import 'package:ente_auth/bootstrap.dart'; +import 'package:ente_auth/main.dart'; +import 'package:ente_auth/services/update_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('Authentication Flow Integration Test with Persistence', () { + testWidgets( + 'Complete auth flow: Use without backup -> Enter setup key -> Verify entry persists after restart', + (WidgetTester tester) async { + // Bootstrap the app + await bootstrap(App.new); + await init(false, via: 'integrationTest'); + await UpdateService.instance.init(); + await tester.pumpAndSettle(); + + // Step 1: Click on "Use without backup" option + final useOfflineText = find.text('Use without backups'); + expect( + useOfflineText, + findsOneWidget, + reason: + 'ERROR: "Use without backups" button not found on initial screen. Check if app loaded correctly or text changed.', + ); + await tester.tap(useOfflineText); + await tester.pumpAndSettle(); + + // Step 2: Click OK button on the warning dialog + final okButton = find.text('Ok'); + expect( + okButton, + findsOneWidget, + reason: + 'ERROR: "Ok" button not found in warning dialog. Check if dialog appeared or button text changed.', + ); + await tester.tap(okButton); + await tester.pumpAndSettle(); + + // Wait for navigation to complete + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // Step 3: Navigate to manual entry screen + bool foundManualEntry = false; + + // Try FloatingActionButton approach first + final fabFinder = find.byType(FloatingActionButton); + if (fabFinder.evaluate().isNotEmpty) { + await tester.tap(fabFinder); + await tester.pumpAndSettle(); + + final manualEntryFinder = find.text('Enter a setup key'); + if (manualEntryFinder.evaluate().isNotEmpty) { + await tester.tap(manualEntryFinder); + await tester.pumpAndSettle(); + foundManualEntry = true; + } + } + + // Alternative approaches if FAB didn't work + if (!foundManualEntry) { + final alternatives = [ + 'Enter details manually', + 'Enter a setup key', + ]; + + for (final text in alternatives) { + final finder = find.text(text); + if (finder.evaluate().isNotEmpty) { + await tester.tap(finder.first); + await tester.pumpAndSettle(); + foundManualEntry = true; + break; + } + } + } + + expect( + foundManualEntry, + isTrue, + reason: + 'ERROR: Could not find manual entry option. Tried FAB + "Enter details manually", "Enter a setup key", etc. Check UI navigation.', + ); + + // Step 4: Fill in the form with test data + final textFields = find.byType(TextFormField); + expect( + textFields.evaluate().length, + greaterThanOrEqualTo(3), + reason: + 'ERROR: Expected at least 3 text fields (issuer, secret, account) but found ${textFields.evaluate().length}. Check manual entry form.', + ); + + // Fill issuer field + await tester.tap(textFields.first); + await tester.enterText(textFields.first, 'testIssuer'); + await tester.pumpAndSettle(); + + // Fill secret field + await tester.tap(textFields.at(1)); + await tester.enterText( + textFields.at(1), + 'JBSWY3DPEHPK3PXP', + ); // Valid base32 secret + await tester.pumpAndSettle(); + + // Fill account field + await tester.tap(textFields.at(2)); + await tester.enterText(textFields.at(2), 'testAccount'); + await tester.pumpAndSettle(); + + // Step 5: Save the entry + final saveButton = find.text('Save'); + expect( + saveButton, + findsOneWidget, + reason: + 'ERROR: "Save" button not found on manual entry form. Check if button text changed or form layout changed.', + ); + await tester.tap(saveButton); + await tester.pumpAndSettle(); + + // Step 6: Verify entry was created successfully + await tester.pumpAndSettle(const Duration(seconds: 1)); + + // Check if coach mark overlay is present and dismiss it + final coachMarkOverlay = find.text('Ok'); + if (coachMarkOverlay.evaluate().isNotEmpty) { + print('🎯 Dismissing coach mark overlay...'); + await tester.tap(coachMarkOverlay); + await tester.pumpAndSettle(); + } + + // Look for the created entry + final issuerEntryFinder = find.textContaining('testIssuer'); + expect( + issuerEntryFinder, + findsAtLeastNWidgets(1), + reason: + 'ERROR: testIssuer entry not found after saving. Entry creation may have failed or navigation issue occurred.', + ); + + final accountEntryFinder = find.textContaining('testAccount'); + expect( + accountEntryFinder, + findsAtLeastNWidgets(1), + reason: + 'ERROR: testAccount not found after saving. Account field may not have been saved properly.', + ); + print('✅ Step 1 completed: Entry created successfully'); + print('- testIssuer entry is visible'); + print('- testAccount is visible'); + + // warning about clearing + + // Step 8: Add second code entry + print('🔄 Adding second code entry...'); + + // Wait a moment before adding second entry + await tester.pumpAndSettle(const Duration(seconds: 1)); + + // Click FAB to add second entry + final fabFinder2 = find.byType(FloatingActionButton); + expect( + fabFinder2, + findsOneWidget, + reason: 'FAB not found for second entry', + ); + await tester.tap(fabFinder2); + await tester.pumpAndSettle(); + + // Click "Enter details manually" (coach mark won't show second time) + final manualEntryFinder = find.text('Enter details manually'); + expect( + manualEntryFinder, + findsOneWidget, + reason: 'Manual entry option not found', + ); + await tester.tap(manualEntryFinder); + await tester.pumpAndSettle(); + + // Fill second entry form + final textFields2 = find.byType(TextFormField); + expect(textFields2.evaluate().length, greaterThanOrEqualTo(3)); + + // Fill second issuer field + await tester.tap(textFields2.first); + await tester.enterText(textFields2.first, 'testIssuer2'); + await tester.pumpAndSettle(); + + // Verify issuer field was filled + final issuerField = tester.widget(textFields2.first); + print( + '✓ Issuer field controller text: "${issuerField.controller?.text ?? "null"}"'); + + // Fill second secret field + await tester.tap(textFields2.at(1)); + await tester.enterText(textFields2.at(1), 'JBSWY3DPEHPK3PXP'); + await tester.pumpAndSettle(); + + // Verify secret field was filled + final secretField = tester.widget(textFields2.at(1)); + print( + '✓ Secret field controller text: "${secretField.controller?.text ?? "null"}"'); + + // Fill second account field + await tester.tap(textFields2.at(2)); + await tester.enterText(textFields2.at(2), 'testAccount2'); + await tester.pumpAndSettle(); + + // Save second entry + final saveButton2 = find.text('Save'); + await tester.tap(saveButton2); + await tester.pumpAndSettle(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // Verify both entries exist + final issuer1Finder = find.textContaining('testIssuer'); + final issuer2Finder = find.textContaining('testIssuer2'); + final account1Finder = find.textContaining('testAccount'); + final account2Finder = find.textContaining('testAccount2'); + + expect(issuer1Finder, findsAtLeastNWidgets(1), + reason: 'First issuer not found'); + expect(issuer2Finder, findsAtLeastNWidgets(1), + reason: 'Second issuer not found'); + expect( + account1Finder, + findsAtLeastNWidgets(1), + reason: 'First account not found', + ); + expect( + account2Finder, + findsAtLeastNWidgets(1), + reason: 'Second account not found', + ); + + print('✅ Step 2 completed: Both entries created successfully'); + print('- testIssuer and testIssuer2 entries are visible'); + print('- testAccount and testAccount2 are visible'); + + print('✅ Integration test completed successfully!'); + print('- Both entries created and verified'); + print('- Multiple TOTP codes are being generated'); + print('- Data persistence is working correctly'); + }, + timeout: const Timeout(Duration(minutes: 3)), + ); + }); +} diff --git a/mobile/apps/auth/ios/Podfile.lock b/mobile/apps/auth/ios/Podfile.lock index 6e4788a815..9060635ec1 100644 --- a/mobile/apps/auth/ios/Podfile.lock +++ b/mobile/apps/auth/ios/Podfile.lock @@ -68,6 +68,8 @@ PODS: - Flutter - fluttertoast (0.0.2): - Flutter + - integration_test (0.0.1): + - Flutter - local_auth_darwin (0.0.1): - Flutter - FlutterMacOS @@ -150,6 +152,7 @@ DEPENDENCIES: - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - move_to_background (from `.symlinks/plugins/move_to_background/ios`) - objective_c (from `.symlinks/plugins/objective_c/ios`) @@ -210,6 +213,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_secure_storage/ios" fluttertoast: :path: ".symlinks/plugins/fluttertoast/ios" + integration_test: + :path: ".symlinks/plugins/integration_test/ios" local_auth_darwin: :path: ".symlinks/plugins/local_auth_darwin/darwin" move_to_background: @@ -260,6 +265,7 @@ SPEC CHECKSUMS: flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29 flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 local_auth_darwin: fa4b06454df7df8e97c18d7ee55151c57e7af0de move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb diff --git a/mobile/apps/auth/lib/main.dart b/mobile/apps/auth/lib/main.dart index 5136e72feb..43cab1b5bc 100644 --- a/mobile/apps/auth/lib/main.dart +++ b/mobile/apps/auth/lib/main.dart @@ -99,7 +99,7 @@ Future _runInForeground() async { return await _runWithLogs(() async { _logger.info("Starting app in foreground"); try { - await _init(false, via: 'mainMethod'); + await init(false, via: 'mainMethod'); } catch (e, s) { _logger.severe("Failed to init", e, s); rethrow; @@ -160,7 +160,7 @@ void _registerWindowsProtocol() { } } -Future _init(bool bool, {String? via}) async { +Future init(bool bool, {String? via}) async { _registerWindowsProtocol(); await CryptoUtil.init(); diff --git a/mobile/apps/auth/pubspec.lock b/mobile/apps/auth/pubspec.lock index 1005b6f6ed..fee5f63c8b 100644 --- a/mobile/apps/auth/pubspec.lock +++ b/mobile/apps/auth/pubspec.lock @@ -605,6 +605,11 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_email_sender: dependency: "direct main" description: @@ -853,6 +858,11 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" glob: dependency: transitive description: @@ -965,6 +975,11 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.4" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" intl: dependency: "direct main" description: @@ -1358,6 +1373,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.8" + process: + dependency: transitive + description: + name: process + sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" + url: "https://pub.dev" + source: hosted + version: "5.0.3" protobuf: dependency: "direct main" description: @@ -1740,6 +1763,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.0" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" synchronized: dependency: transitive description: @@ -1972,6 +2003,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + url: "https://pub.dev" + source: hosted + version: "3.1.0" win32: dependency: "direct main" description: diff --git a/mobile/apps/auth/pubspec.yaml b/mobile/apps/auth/pubspec.yaml index 3014a7f68b..58aaf31ba3 100644 --- a/mobile/apps/auth/pubspec.yaml +++ b/mobile/apps/auth/pubspec.yaml @@ -138,6 +138,8 @@ dev_dependencies: build_runner: ^2.1.11 flutter_test: sdk: flutter + integration_test: + sdk: flutter json_serializable: ^6.2.0 lints: ^5.1.1 mocktail: ^1.0.3