import 'package:flutter_test/flutter_test.dart'; import 'package:halkyon_app/halkyon_app.dart'; import 'package:halkyon_app/testing.dart'; import 'package:material_ui/material_ui.dart'; /// A signed-in person's app as a test makes one: whose it is, and the /// account's tiles in the app's own words. class const _PersonApp(final SignedIn person) extends StatelessWidget { @override Widget build(BuildContext context) => MaterialApp( localizationsDelegates: testLocalizationsDelegates, supportedLocales: HalkyonAppLocalizations.supportedLocales, home: Scaffold( body: ListView( children: [ Text('App of ${person.authUserId}'), const AccountTiles( anonymousNote: 'Your notes stay on this device.', signOutAnonymousMessage: "This account can't be signed into again, so signing out " 'deletes it with its notes.', deleteMessage: 'Your notes are deleted everywhere.', ), ], ), ), ); } void main() { late FakeAuthGateway gateway; late TestAppDependencies dependencies; Future start(WidgetTester tester, {InstanceChoice? choice}) async { dependencies = TestAppDependencies(gateway: gateway); final gate = AppScope( dependencies: dependencies, child: SessionGate( look: (context) => (themeMode: ThemeMode.light, locale: null), app: _PersonApp.new, ), ); await tester.pumpWidget( AppFrameScope( frame: testFrame(), child: switch (choice) { final choice? => InstanceScope( notifier: choice, checker: FakeInstanceChecker(), countryNames: MemoryCountryNames(const {}), subdivisionNames: const MemorySubdivisionNames({}), child: gate, ), null => gate, }, ), ); await tester.pumpAndSettle(); } setUp(() => gateway = FakeAuthGateway()); testWidgets('says whose account it is, anonymous on which instance, with ' "the app's note", (tester) async { await start(tester); expect(find.text('Anonymous on test.example.org'), findsOneWidget); expect(find.text('Your notes stay on this device.'), findsOneWidget); expect( find.text('Switch account'), findsNothing, reason: 'no instances to switch among here', ); expect(find.text('Sign out'), findsOneWidget); expect(find.text('Delete account'), findsOneWidget); }); testWidgets('says the account is signed in, without the note', ( tester, ) async { gateway = FakeAuthGateway( status: const SignedIn(authUserId: 'github-person', isIdentified: true), ); await start(tester); expect(find.text('Signed in on test.example.org'), findsOneWidget); expect(find.text('Your notes stay on this device.'), findsNothing); }); testWidgets('switching account lists the accounts on the device, the ' 'instance in use named', (tester) async { final instance = Instance.parse('learn.example.org')!; final choice = InstanceChoice( MemoryInstanceStore((current: instance, known: [instance])), builtIn: instance, forget: (instance) async {}, ); await choice.load(usedBefore: false); await start(tester, choice: choice); expect(find.text('Switch account'), findsOneWidget); expect(find.text('learn.example.org'), findsWidgets); await tester.tap(find.text('Switch account')); await tester.pumpAndSettle(); expect(find.text('Accounts'), findsOneWidget); }); testWidgets('signing out of an anonymous account asks first, in the ' "app's words, as it deletes it; Cancel keeps it", (tester) async { await start(tester); await tester.tap(find.text('Sign out')); await tester.pumpAndSettle(); expect(find.text('Sign out?'), findsOneWidget); expect( find.text( "This account can't be signed into again, so signing out deletes " 'it with its notes.', ), findsOneWidget, ); await tester.tap(find.text('Cancel')); await tester.pumpAndSettle(); expect(find.text('App of anonymous-1'), findsOneWidget); expect(gateway.signOuts, 0); await tester.tap(find.text('Sign out')); await tester.pumpAndSettle(); await tester.tap(find.text('Sign out').last); await tester.pumpAndSettle(); expect(gateway.status, const SignedOut()); expect(find.text('Signed out'), findsOneWidget); }); testWidgets('signing out of an identified account asks nothing', ( tester, ) async { gateway = FakeAuthGateway( status: const SignedIn(authUserId: 'github-person', isIdentified: true), ); await start(tester); await tester.tap(find.text('Sign out')); await tester.pumpAndSettle(); expect(find.text('Sign out?'), findsNothing); expect(gateway.status, const SignedOut()); }); testWidgets("deleting the account asks first, in the app's words; Cancel " 'keeps it', (tester) async { await start(tester); await tester.tap(find.text('Delete account')); await tester.pumpAndSettle(); expect(find.text('Delete your account?'), findsOneWidget); expect(find.text('Your notes are deleted everywhere.'), findsOneWidget); await tester.tap(find.text('Cancel')); await tester.pumpAndSettle(); expect(gateway.accountDeletions, 0); await tester.tap(find.text('Delete account')); await tester.pumpAndSettle(); await tester.tap(find.text('Delete account').last); await tester.pumpAndSettle(); expect(gateway.accountDeletions, 1); expect(find.text('Signed out'), findsOneWidget); }); testWidgets('deleting an account whose setup never finished asks for the ' 'password and a code by mail', (tester) async { // The first person the fake password gateway makes. const person = '00000000-0000-4000-8000-000000000001'; gateway = FakeAuthGateway( status: const SignedIn(authUserId: person, isIdentified: true), ); await start(tester); dependencies.passwordGateway.addAccount( 'anna@example.org', password: 'correct horse', crypto: dependencies.crypto, finished: false, ); dependencies.addresses.addresses[person] = 'anna@example.org'; Finder field(String label) => find.widgetWithText(TextField, label); await tester.tap(find.text('Delete account')); await tester.pumpAndSettle(); await tester.tap(find.text('Delete account').last); await tester.pumpAndSettle(); expect(find.textContaining('a code by mail confirms it'), findsOneWidget); await tester.enterText(field('Current password'), 'correct horse'); await tester.tap(find.text('Email me a code')); await tester.pumpAndSettle(); expect( dependencies.passwordGateway.mailed.single.purpose, MailCodePurpose.deletion, ); expect(find.textContaining('anna@example.org'), findsOneWidget); expect(gateway.accountDeletions, 0); await tester.enterText( field('Code from the email'), dependencies.passwordGateway.codeFor('anna@example.org'), ); await tester.tap(find.text('Delete account').last); await tester.pumpAndSettle(); expect(gateway.accountDeletions, 1); expect( dependencies.passwordGateway.accounts, isNot(contains('anna@example.org')), ); }); testWidgets('deleting an account with a password asks for it and a ' 'code, which a wrong one fails, deleting nothing', (tester) async { // The first person the fake password gateway makes. const person = '00000000-0000-4000-8000-000000000001'; gateway = FakeAuthGateway( status: const SignedIn(authUserId: person, isIdentified: true), ); await start(tester); dependencies.passwordGateway.addAccount( 'anna@example.org', password: 'correct horse', crypto: dependencies.crypto, ); // The address this device signed in with, which deleting sends. dependencies.addresses.addresses[person] = 'anna@example.org'; Finder field(String label) => find.widgetWithText(TextField, label); await tester.tap(find.text('Delete account')); await tester.pumpAndSettle(); await tester.tap(find.text('Delete account').last); await tester.pumpAndSettle(); expect(gateway.accountDeletions, 0); expect(field('Current password'), findsOneWidget); await tester.enterText(field('Current password'), 'wrong horse'); await tester.enterText( field('Code from your authenticator app'), fakeTotpCode, ); await tester.tap(find.text('Delete account').last); await tester.pumpAndSettle(); expect( find.text("The address, the password or the code isn't right."), findsOneWidget, ); expect(gateway.accountDeletions, 0); expect(dependencies.passwordGateway.accounts, contains('anna@example.org')); await tester.enterText(field('Current password'), 'correct horse'); await tester.tap(find.text('Delete account').last); await tester.pumpAndSettle(); expect(gateway.accountDeletions, 1); expect( dependencies.passwordGateway.accounts, isNot(contains('anna@example.org')), ); expect(find.text('Signed out'), findsOneWidget); }); testWidgets('speaks German', (tester) async { await start(tester); final german = lookupHalkyonAppLocalizations(const Locale('de')); expect( german.accountAnonymousOn('test.example.org'), 'Anonym auf test.example.org', ); expect(german.accountSignedInOn('x'), 'Angemeldet auf x'); expect(german.accountSwitch, 'Konto wechseln'); expect(german.accountSignOut, 'Abmelden'); expect(german.accountSignOutTitle, 'Abmelden?'); expect(german.accountDelete, 'Konto löschen'); expect(german.accountDeleteTitle, 'Dein Konto löschen?'); }); }