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>
215 lines
6.9 KiB
Dart
215 lines
6.9 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:halkyon_app/halkyon_app.dart';
|
|
import 'package:halkyon_app/testing.dart';
|
|
|
|
void main() {
|
|
late FakeAuthGateway gateway;
|
|
late SessionBloc bloc;
|
|
|
|
/// An anonymous account's session, left from before they went (ADR 0020,
|
|
/// D25).
|
|
const anonymous = SignedIn(authUserId: 'anonymous-1', isIdentified: false);
|
|
|
|
void start({AuthStatus status = const SignedOut()}) {
|
|
gateway = FakeAuthGateway(status: status);
|
|
bloc = SessionBloc(AuthRepository(gateway, MemorySignInAddresses()));
|
|
}
|
|
|
|
tearDown(() => bloc.close());
|
|
|
|
test('nobody signed in, it asks how to start, making no account (ADR '
|
|
'0020, D4)', () async {
|
|
start();
|
|
|
|
bloc.add(const SessionEventStarted());
|
|
|
|
await expectLater(bloc.stream, emits(const SessionStateSignedOut()));
|
|
expect(gateway.accountDeletions, 0);
|
|
expect(gateway.signOuts, 0);
|
|
});
|
|
|
|
test('keeps a stored session', () async {
|
|
const stored = SignedIn(authUserId: 'stored', isIdentified: true);
|
|
start(status: stored);
|
|
|
|
bloc.add(const SessionEventStarted());
|
|
|
|
await expectLater(bloc.stream, emits(const SessionStateActive(stored)));
|
|
});
|
|
|
|
test(
|
|
"an anonymous account's session left from before ends as the app "
|
|
'starts: the account deleted, the device asked how to start (D25)',
|
|
() async {
|
|
start(status: anonymous);
|
|
|
|
bloc.add(const SessionEventStarted());
|
|
|
|
await expectLater(bloc.stream, emits(const SessionStateSignedOut()));
|
|
expect(gateway.accountDeletions, 1);
|
|
expect(gateway.status, const SignedOut());
|
|
},
|
|
);
|
|
|
|
test('the instance out of reach, the anonymous session still ends on the '
|
|
"device: the account is left to the instance's pruning", () async {
|
|
start(status: anonymous);
|
|
gateway.deletionError = Exception('No connection');
|
|
|
|
bloc.add(const SessionEventStarted());
|
|
|
|
await expectLater(bloc.stream, emits(const SessionStateSignedOut()));
|
|
expect(gateway.signOuts, 1);
|
|
expect(gateway.status, const SignedOut());
|
|
});
|
|
|
|
test("an anonymous session the device can't even end never opens its "
|
|
'app: the start screen all the same', () async {
|
|
start(status: anonymous);
|
|
gateway
|
|
..deletionError = Exception('No connection')
|
|
..signOutError = Exception('The keyring is locked');
|
|
|
|
bloc.add(const SessionEventStarted());
|
|
|
|
await expectLater(bloc.stream, emits(const SessionStateSignedOut()));
|
|
expect(gateway.status, anonymous);
|
|
});
|
|
|
|
test('after signing out, asks how to start, deleting nothing', () async {
|
|
const stored = SignedIn(authUserId: 'stored', isIdentified: true);
|
|
start(status: stored);
|
|
bloc.add(const SessionEventStarted());
|
|
await expectLater(bloc.stream, emits(const SessionStateActive(stored)));
|
|
|
|
bloc.add(const SessionEventSignOutRequested());
|
|
|
|
// The person's app stays while signing out, never the launch screen in
|
|
// between.
|
|
await expectLater(
|
|
bloc.stream,
|
|
emitsInOrder([
|
|
const SessionStateActive(stored, busy: SessionAction.signOut),
|
|
const SessionStateSignedOut(),
|
|
]),
|
|
);
|
|
expect(gateway.accountDeletions, 0);
|
|
});
|
|
|
|
test('a session the server ended asks how to start too', () async {
|
|
const stored = SignedIn(authUserId: 'person-1', isIdentified: true);
|
|
start(status: stored);
|
|
bloc.add(const SessionEventStarted());
|
|
await expectLater(bloc.stream, emits(const SessionStateActive(stored)));
|
|
|
|
gateway.revokeSession();
|
|
|
|
await expectLater(bloc.stream, emits(const SessionStateSignedOut()));
|
|
});
|
|
|
|
test('deleting the account asks how to start', () async {
|
|
const stored = SignedIn(authUserId: 'person-1', isIdentified: true);
|
|
start(status: stored);
|
|
bloc.add(const SessionEventStarted());
|
|
await expectLater(bloc.stream, emits(const SessionStateActive(stored)));
|
|
|
|
bloc.add(const SessionEventDeleteRequested());
|
|
|
|
await expectLater(
|
|
bloc.stream,
|
|
emitsInOrder([
|
|
const SessionStateActive(stored, busy: SessionAction.delete),
|
|
const SessionStateSignedOut(),
|
|
]),
|
|
);
|
|
expect(gateway.accountDeletions, 1);
|
|
});
|
|
|
|
test('a deletion that fails keeps the person signed in, their app in '
|
|
'place, and says so; asked again, it tries again', () async {
|
|
const stored = SignedIn(authUserId: 'person-1', isIdentified: true);
|
|
start(status: stored);
|
|
bloc.add(const SessionEventStarted());
|
|
await expectLater(bloc.stream, emits(const SessionStateActive(stored)));
|
|
gateway.deletionError = Exception('No connection');
|
|
|
|
bloc.add(const SessionEventDeleteRequested());
|
|
|
|
await expectLater(
|
|
bloc.stream,
|
|
emitsInOrder([
|
|
const SessionStateActive(stored, busy: SessionAction.delete),
|
|
const SessionStateActive(stored, problem: SessionProblem.deleteFailed),
|
|
]),
|
|
);
|
|
expect(gateway.status, stored);
|
|
|
|
gateway.deletionError = null;
|
|
bloc.add(const SessionEventDeleteRequested());
|
|
|
|
await expectLater(
|
|
bloc.stream,
|
|
emitsInOrder([
|
|
const SessionStateActive(stored, busy: SessionAction.delete),
|
|
const SessionStateSignedOut(),
|
|
]),
|
|
);
|
|
expect(gateway.accountDeletions, 2);
|
|
});
|
|
|
|
test('a sign-out that fails keeps the person signed in, their app in '
|
|
'place, and says so', () async {
|
|
const stored = SignedIn(authUserId: 'person-1', isIdentified: true);
|
|
start(status: stored);
|
|
bloc.add(const SessionEventStarted());
|
|
await expectLater(bloc.stream, emits(const SessionStateActive(stored)));
|
|
gateway.signOutError = Exception('The keyring is locked');
|
|
|
|
bloc.add(const SessionEventSignOutRequested());
|
|
|
|
await expectLater(
|
|
bloc.stream,
|
|
emitsInOrder([
|
|
const SessionStateActive(stored, busy: SessionAction.signOut),
|
|
const SessionStateActive(stored, problem: SessionProblem.signOutFailed),
|
|
]),
|
|
);
|
|
expect(gateway.status, stored);
|
|
});
|
|
|
|
group("an account waiting for the instance's operator (Halkyon Learn's "
|
|
'ADR 0021, D31)', () {
|
|
final wait = fakeWaitOf('anna', handle: 'anna');
|
|
|
|
test('a wait kept from before shows as the app starts', () async {
|
|
start(status: Waiting(wait));
|
|
|
|
bloc.add(const SessionEventStarted());
|
|
|
|
await expectLater(bloc.stream, emits(SessionStateWaiting(wait)));
|
|
});
|
|
|
|
test('follows the wait as it changes, and as it ends', () async {
|
|
start();
|
|
bloc.add(const SessionEventStarted());
|
|
await expectLater(bloc.stream, emits(const SessionStateSignedOut()));
|
|
final states = <SessionState>[];
|
|
final listening = bloc.stream.listen(states.add);
|
|
|
|
await gateway.wait(wait);
|
|
await pumpEventQueue();
|
|
gateway.waitAnswer = AccountWaitState.approved;
|
|
await gateway.lookWhileWaiting();
|
|
await pumpEventQueue();
|
|
await gateway.stopWaiting();
|
|
await pumpEventQueue();
|
|
await listening.cancel();
|
|
|
|
expect(states, [
|
|
SessionStateWaiting(wait),
|
|
SessionStateWaiting(wait.standing(AccountWaitState.approved)),
|
|
const SessionStateSignedOut(),
|
|
]);
|
|
});
|
|
});
|
|
}
|