An account signed in or up to on an instance whose operator approves new accounts, or one suspended, gets no session but a wait: the device keeps its pass, words and handle in the keyring per instance (SecureAccountWaitStore, InstanceStorage.waitOf), the auth status is Waiting, and SessionGate shows WaitScreen in its own app, so the start screen's sheets go with it. Every sign-in flow hands over to it: the password's sign-in, sign-up, reset and account reset, SimpleLogin's sign-in, sign-up and mending; their cubits show no problem then. The wait screen shows the handle, the four words the operator reads out for the person to match, and that Halkyon never asks anyone to read them out (D33); it asks where the wait stands as it opens, every 30 seconds, as the app comes back and when asked. A new account can be deleted with its pass, a suspended one can't (ADR 0017). Approved, it leads to the usual sign-in, once more as before (D31); a pass the instance knows no more ends the wait. The start screen says where new accounts wait for approval, and offers no Create an account where the operator takes none; both refusals (closed, too many waiting) have their words, en and de. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
234 lines
7.3 KiB
Dart
234 lines
7.3 KiB
Dart
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.
|
|
class const _PersonApp(final SignedIn person) extends StatelessWidget {
|
|
@override
|
|
Widget build(BuildContext context) => MaterialApp(
|
|
home: Scaffold(body: Text('App of ${person.authUserId}')),
|
|
);
|
|
}
|
|
|
|
/// The wait screen (Halkyon Learn's ADR 0021, D31, D33), as the session gate
|
|
/// shows it while an account signed in or up to here waits for the
|
|
/// instance's operator.
|
|
void main() {
|
|
late FakeAuthGateway gateway;
|
|
late TestAppDependencies dependencies;
|
|
final anna = fakeWaitOf('anna', handle: 'anna');
|
|
|
|
setUp(() {
|
|
gateway = FakeAuthGateway(status: Waiting(anna));
|
|
dependencies = TestAppDependencies(gateway: gateway);
|
|
});
|
|
|
|
Future<void> start(WidgetTester tester) async {
|
|
tester.view.physicalSize = const Size(412, 1200);
|
|
tester.view.devicePixelRatio = 1;
|
|
addTearDown(tester.view.reset);
|
|
await tester.pumpWidget(
|
|
AppFrameScope(
|
|
frame: testFrame(),
|
|
child: AppScope(
|
|
dependencies: dependencies,
|
|
child: SessionGate(
|
|
look: (context) => (themeMode: ThemeMode.light, locale: null),
|
|
app: _PersonApp.new,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
Future<void> tap(WidgetTester tester, String label) async {
|
|
final button = find.text(label);
|
|
await tester.ensureVisible(button);
|
|
await tester.tap(button);
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
const waiting =
|
|
'Your account waits until the operator of test.example.org approves it.';
|
|
|
|
testWidgets('shows the handle, the words to match and what to do with '
|
|
'them: only check them, never read them out (D33)', (tester) async {
|
|
await start(tester);
|
|
|
|
expect(find.text('Halkyon Test'), findsOneWidget);
|
|
expect(find.text('@anna'), findsOneWidget);
|
|
expect(find.text(waiting), findsOneWidget);
|
|
expect(find.text('Matching code'), findsOneWidget);
|
|
for (final word in fakeMatchingWords) {
|
|
expect(find.text(word), findsOneWidget);
|
|
}
|
|
expect(
|
|
find.text(
|
|
'When the operator of test.example.org reads you four words, check '
|
|
'that they are these. Never read them out yourself: Halkyon never '
|
|
'asks anyone to.',
|
|
),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.text(anna.pass), findsNothing, reason: 'the pass is a secret');
|
|
expect(find.text('Start on test.example.org:'), findsNothing);
|
|
});
|
|
|
|
testWidgets('asks the instance as it opens, every 30 seconds, as the app '
|
|
'comes back, and when asked', (tester) async {
|
|
await start(tester);
|
|
expect(gateway.looks, 1);
|
|
|
|
await tester.pump(const Duration(seconds: 30));
|
|
expect(gateway.looks, 2);
|
|
|
|
// Away and back, as the system moves an app.
|
|
[
|
|
AppLifecycleState.inactive,
|
|
AppLifecycleState.hidden,
|
|
AppLifecycleState.paused,
|
|
AppLifecycleState.hidden,
|
|
AppLifecycleState.inactive,
|
|
AppLifecycleState.resumed,
|
|
].forEach(tester.binding.handleAppLifecycleStateChanged);
|
|
await tester.pump();
|
|
expect(gateway.looks, 3);
|
|
|
|
await tap(tester, 'Check now');
|
|
expect(gateway.looks, 4);
|
|
});
|
|
|
|
testWidgets('approved, it says so and leads to the usual sign-in, once '
|
|
'more as before (D31): the pass holds no session', (tester) async {
|
|
await start(tester);
|
|
|
|
gateway.waitAnswer = AccountWaitState.approved;
|
|
await tester.pump(const Duration(seconds: 30));
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
find.text(
|
|
'The operator of test.example.org has approved your account. Sign '
|
|
'in once more, as you did before.',
|
|
),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.text('Matching code'), findsNothing);
|
|
expect(gateway.usedSessions, isEmpty);
|
|
|
|
// Nothing left to ask.
|
|
await tester.pump(const Duration(seconds: 60));
|
|
expect(gateway.looks, 2);
|
|
|
|
await tap(tester, 'Sign in');
|
|
expect(find.text('Start on test.example.org:'), findsOneWidget);
|
|
expect(gateway.waitsStopped, 1);
|
|
});
|
|
|
|
testWidgets('a wait the instance knows no more ends: refused, or too old', (
|
|
tester,
|
|
) async {
|
|
await start(tester);
|
|
|
|
gateway.waitAnswer = AccountWaitState.ended;
|
|
await tap(tester, 'Check now');
|
|
|
|
expect(
|
|
find.text(
|
|
'This sign-up has ended: the operator of test.example.org refused '
|
|
'it, or it waited too long.',
|
|
),
|
|
findsOneWidget,
|
|
);
|
|
await tap(tester, 'Back to the start');
|
|
expect(find.text('Start on test.example.org:'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('a new account is deleted with its pass, after asking', (
|
|
tester,
|
|
) async {
|
|
await start(tester);
|
|
|
|
await tap(tester, 'Delete this account');
|
|
expect(find.text('Delete this account?'), findsOneWidget);
|
|
await tap(tester, 'Cancel');
|
|
expect(gateway.waitingDeletions, 0);
|
|
expect(find.text(waiting), findsOneWidget);
|
|
|
|
await tap(tester, 'Delete this account');
|
|
await tester.tap(find.widgetWithText(FilledButton, 'Delete this account'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(gateway.waitingDeletions, 1);
|
|
expect(find.text('Start on test.example.org:'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('a deletion that fails says so, and the wait stays', (
|
|
tester,
|
|
) async {
|
|
gateway.waitingDeletionError = Exception('No connection');
|
|
await start(tester);
|
|
|
|
await tap(tester, 'Delete this account');
|
|
await tester.tap(find.widgetWithText(FilledButton, 'Delete this account'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
find.text("Couldn't delete the account. Try again."),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.text(waiting), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('a suspended account shows its words but offers no deletion: '
|
|
"that takes its owner's password and a code (ADR 0017)", (tester) async {
|
|
gateway = FakeAuthGateway(
|
|
status: Waiting(anna.standing(AccountWaitState.suspended)),
|
|
);
|
|
dependencies = TestAppDependencies(gateway: gateway);
|
|
await start(tester);
|
|
|
|
expect(
|
|
find.text(
|
|
'The operator of test.example.org has suspended your account. Only '
|
|
'they can lift it.',
|
|
),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.text('abandon'), findsOneWidget);
|
|
expect(find.text('Delete this account'), findsNothing);
|
|
expect(find.text('Sign out'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('an instance out of reach is said so, and asked again', (
|
|
tester,
|
|
) async {
|
|
gateway.lookError = Exception('No connection');
|
|
await start(tester);
|
|
|
|
expect(
|
|
find.text(
|
|
"test.example.org couldn't be reached. It's asked again every 30 "
|
|
'seconds.',
|
|
),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.text(waiting), findsOneWidget);
|
|
|
|
gateway.lookError = null;
|
|
await tester.pump(const Duration(seconds: 30));
|
|
await tester.pumpAndSettle();
|
|
expect(find.textContaining("couldn't be reached"), findsNothing);
|
|
});
|
|
|
|
testWidgets('signing out stops waiting on this device: the start screen '
|
|
'follows, where signing in again waits anew', (tester) async {
|
|
await start(tester);
|
|
|
|
await tap(tester, 'Sign out');
|
|
|
|
expect(gateway.waitsStopped, 1);
|
|
expect(find.text('Start on test.example.org:'), findsOneWidget);
|
|
});
|
|
}
|