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>
255 lines
8.2 KiB
Dart
255 lines
8.2 KiB
Dart
import 'package:flutter_bloc/flutter_bloc.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, and a way to
|
|
/// sign out.
|
|
class const _PersonApp(final SignedIn person) extends StatelessWidget {
|
|
@override
|
|
Widget build(BuildContext context) => MaterialApp(
|
|
localizationsDelegates: testLocalizationsDelegates,
|
|
supportedLocales: HalkyonAppLocalizations.supportedLocales,
|
|
home: Scaffold(
|
|
body: Builder(
|
|
builder: (context) => Column(
|
|
children: [
|
|
Text('App of ${person.authUserId}'),
|
|
TextButton(
|
|
onPressed: () => context.read<SessionBloc>().add(
|
|
const SessionEventSignOutRequested(),
|
|
),
|
|
child: const Text('Sign out'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void main() {
|
|
late FakeAuthGateway gateway;
|
|
late TestAppDependencies dependencies;
|
|
|
|
setUp(() {
|
|
gateway = FakeAuthGateway();
|
|
dependencies = TestAppDependencies(gateway: gateway);
|
|
});
|
|
|
|
Future<void> start(WidgetTester tester) async {
|
|
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();
|
|
}
|
|
|
|
/// Nobody signed in, as at a first start, on an instance offering every
|
|
/// way in.
|
|
void signedOut() {
|
|
gateway = FakeAuthGateway(status: const SignedOut());
|
|
dependencies = TestAppDependencies(gateway: gateway);
|
|
dependencies.simpleLoginGateway.isOffered = true;
|
|
}
|
|
|
|
testWidgets("shows the signed-in person's app", (tester) async {
|
|
await start(tester);
|
|
|
|
expect(find.text('App of person-1'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('nobody signed in, it asks how to start on the instance, '
|
|
'making no account: there is no starting without one (ADR 0020, D4)', (
|
|
tester,
|
|
) async {
|
|
signedOut();
|
|
await start(tester);
|
|
|
|
expect(find.text('Halkyon Test'), findsOneWidget);
|
|
expect(find.text('Start on test.example.org:'), findsOneWidget);
|
|
expect(find.text('Continue with SimpleLogin'), findsOneWidget);
|
|
expect(find.text('Sign in'), findsOneWidget);
|
|
expect(find.text('Create an account'), findsOneWidget);
|
|
expect(
|
|
find.textContaining('GitHub'),
|
|
findsNothing,
|
|
reason: 'GitHub signs nobody in any more (ADR 0020, D36)',
|
|
);
|
|
expect(find.text('Start without an account'), findsNothing);
|
|
expect(
|
|
find.text('Use another instance'),
|
|
findsNothing,
|
|
reason: 'no instances to choose among here',
|
|
);
|
|
expect(gateway.status, const SignedOut());
|
|
});
|
|
|
|
testWidgets("an anonymous account's session left from before ends as the "
|
|
'app starts, the account deleted, and the start screen asks (D25)', (
|
|
tester,
|
|
) async {
|
|
gateway = FakeAuthGateway(
|
|
status: const SignedIn(authUserId: 'anonymous-1', isIdentified: false),
|
|
);
|
|
dependencies = TestAppDependencies(gateway: gateway);
|
|
await start(tester);
|
|
|
|
expect(find.text('App of anonymous-1'), findsNothing);
|
|
expect(find.text('Start on test.example.org:'), findsOneWidget);
|
|
expect(gateway.accountDeletions, 1);
|
|
});
|
|
|
|
testWidgets('an instance with no way to create an account says so', (
|
|
tester,
|
|
) async {
|
|
signedOut();
|
|
dependencies.passwordGateway.offered = false;
|
|
dependencies.simpleLoginGateway.isOffered = false;
|
|
await start(tester);
|
|
|
|
expect(
|
|
find.text('test.example.org offers no way to create an account.'),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.text('Continue with SimpleLogin'), findsNothing);
|
|
expect(find.text('Create an account'), findsNothing);
|
|
});
|
|
|
|
testWidgets('offers only the ways the instance has', (tester) async {
|
|
signedOut();
|
|
dependencies.passwordGateway.offered = false;
|
|
await start(tester);
|
|
|
|
expect(find.text('Continue with SimpleLogin'), findsOneWidget);
|
|
expect(find.text('Sign in'), findsNothing);
|
|
expect(find.text('Create an account'), findsNothing);
|
|
expect(find.textContaining('offers no way'), findsNothing);
|
|
});
|
|
|
|
testWidgets('an instance out of reach is said so, offering nothing, and '
|
|
'asked again', (tester) async {
|
|
signedOut();
|
|
dependencies.passwordGateway.failing = const PasswordFailed(
|
|
PasswordProblem.failed,
|
|
);
|
|
dependencies.simpleLoginGateway.failing = const SimpleLoginFailed(
|
|
SimpleLoginProblem.failed,
|
|
);
|
|
await start(tester);
|
|
|
|
expect(find.text("Couldn't reach the server."), findsOneWidget);
|
|
expect(find.text('Continue with SimpleLogin'), findsNothing);
|
|
expect(find.textContaining('offers no way'), findsNothing);
|
|
|
|
dependencies.passwordGateway.failing = null;
|
|
dependencies.simpleLoginGateway.failing = null;
|
|
await tester.tap(find.text('Try again'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Continue with SimpleLogin'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('signing out, or the instance ending the session, asks how to '
|
|
'start', (tester) async {
|
|
gateway = FakeAuthGateway(
|
|
status: const SignedIn(authUserId: 'ada', isIdentified: true),
|
|
);
|
|
dependencies = TestAppDependencies(gateway: gateway);
|
|
await start(tester);
|
|
expect(find.text('App of ada'), findsOneWidget);
|
|
|
|
await tester.tap(find.text('Sign out'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Start on test.example.org:'), findsOneWidget);
|
|
|
|
// Signed in anew, as a sign-in's session put in place does it.
|
|
await gateway.useSession(const SessionGrant('ada', 'session-2'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('App of ada'), findsOneWidget);
|
|
gateway.revokeSession();
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Start on test.example.org:'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets("a sign-out under way or failed leaves the person's app as it "
|
|
'is, built once', (tester) async {
|
|
gateway = FakeAuthGateway(
|
|
status: const SignedIn(authUserId: 'ada', isIdentified: true),
|
|
)..signOutError = Exception('The keyring is locked');
|
|
dependencies = TestAppDependencies(gateway: gateway);
|
|
var builds = 0;
|
|
await tester.pumpWidget(
|
|
AppFrameScope(
|
|
frame: testFrame(),
|
|
child: AppScope(
|
|
dependencies: dependencies,
|
|
child: SessionGate(
|
|
look: (context) => (themeMode: ThemeMode.light, locale: null),
|
|
app: (person) {
|
|
builds++;
|
|
return _PersonApp(person);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(builds, 1);
|
|
|
|
await tester.tap(find.text('Sign out'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('App of ada'), findsOneWidget);
|
|
expect(builds, 1, reason: "busy and its problem are the account tiles'");
|
|
});
|
|
|
|
testWidgets("where the instance's operator takes no new accounts, nobody "
|
|
"is offered to create one (Halkyon Learn's ADR 0021, D19)", (
|
|
tester,
|
|
) async {
|
|
signedOut();
|
|
gateway.offeredSignUps = SignUps.closed;
|
|
await start(tester);
|
|
|
|
expect(
|
|
find.text('test.example.org takes no new accounts: sign in with yours.'),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.text('Create an account'), findsNothing);
|
|
expect(find.text('Sign in'), findsOneWidget);
|
|
expect(find.text('Continue with SimpleLogin'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('where new accounts wait for approval, the start screen says '
|
|
'so, and offers all as before', (tester) async {
|
|
signedOut();
|
|
gateway.offeredSignUps = SignUps.approval;
|
|
await start(tester);
|
|
|
|
expect(
|
|
find.text(
|
|
'New accounts on test.example.org wait until its operator approves '
|
|
'them.',
|
|
),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.text('Create an account'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('open, the start screen says nothing of it', (tester) async {
|
|
signedOut();
|
|
await start(tester);
|
|
|
|
expect(find.textContaining('new accounts'), findsNothing);
|
|
expect(find.textContaining('New accounts'), findsNothing);
|
|
});
|
|
}
|