feat(app): a wait screen for accounts that wait for the operator (ADR 0021, step 1)
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>
This commit is contained in:
parent
a060fbc21d
commit
7ac715cd97
3 changed files with 2284 additions and 103 deletions
|
|
@ -465,4 +465,62 @@ void main() {
|
|||
}),
|
||||
);
|
||||
});
|
||||
|
||||
group("where new accounts wait for the instance's operator (Halkyon "
|
||||
"Learn's ADR 0021, D19, D31)", () {
|
||||
/// A sign-up up to the code mailed, with the handle `anna`.
|
||||
Future<AccountSetupCubit> upToTheCode() async {
|
||||
final cubit = setUpAs(AccountSetupMode.signUp)
|
||||
..addressEntered('anna@example.org');
|
||||
await cubit.passwordEntered('correct horse', 'correct horse');
|
||||
cubit.phraseWrittenDown();
|
||||
await cubit.phraseChecked(wordsAsked(cubit.state));
|
||||
cubit.handleEntered('anna');
|
||||
return cubit;
|
||||
}
|
||||
|
||||
test('the new account waits: the device keeps its wait in place of a '
|
||||
'session, sets up no authenticator, and the sheet shows no '
|
||||
'problem, as the wait screen takes its place', () async {
|
||||
auth.offeredSignUps = SignUps.approval;
|
||||
final cubit = await upToTheCode();
|
||||
|
||||
final states = await statesOf(
|
||||
cubit,
|
||||
() => cubit.mailCodeEntered(gateway.codeFor('anna@example.org')),
|
||||
);
|
||||
|
||||
final person = gateway.accounts['anna@example.org']!.person;
|
||||
expect(auth.status, Waiting(fakeWaitOf(person, handle: 'anna')));
|
||||
expect(states, [at(AccountSetupStep.mailCode, busy: true)]);
|
||||
expect(gateway.calls, isNot(contains('enrol')));
|
||||
expect(auth.usedSessions, isEmpty);
|
||||
});
|
||||
|
||||
test('closed, the code proves the address first, then the sign-up is '
|
||||
'refused, and nothing is made', () async {
|
||||
auth.offeredSignUps = SignUps.closed;
|
||||
final cubit = await upToTheCode();
|
||||
|
||||
await cubit.mailCodeEntered(gateway.codeFor('anna@example.org'));
|
||||
|
||||
expect(
|
||||
cubit.state,
|
||||
at(AccountSetupStep.mailCode, problem: PasswordProblem.signUpsClosed),
|
||||
);
|
||||
expect(gateway.accounts, isEmpty);
|
||||
expect(auth.status, const SignedOut());
|
||||
});
|
||||
|
||||
test('closed, an account of before (an address, no password) still takes '
|
||||
'its password', () async {
|
||||
auth.offeredSignUps = SignUps.closed;
|
||||
gateway.addAddressOnly('anna@example.org');
|
||||
final cubit = await upToTheCode();
|
||||
|
||||
await cubit.mailCodeEntered(gateway.codeFor('anna@example.org'));
|
||||
|
||||
expect(cubit.state, at(AccountSetupStep.authenticator));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,6 +107,30 @@ void main() {
|
|||
expect(addresses.addresses[anna.person], 'anna@example.org');
|
||||
});
|
||||
|
||||
test("an account waiting for the instance's operator signs in to its "
|
||||
"wait (Halkyon Learn's ADR 0021, D31): the device keeps it, with no "
|
||||
"session, and nobody's keys", () async {
|
||||
await keys.keep('ada', crypto.keysNumbered(1));
|
||||
final wait = fakeWaitOf(
|
||||
anna.person,
|
||||
).standing(AccountWaitState.suspended);
|
||||
gateway.accountWaits[anna.person] = wait;
|
||||
final unlocked = await passwords.unlock(
|
||||
'anna@example.org',
|
||||
'correct horse',
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
passwords.signIn(unlocked, totpCode: fakeTotpCode, language: 'en'),
|
||||
throwsA(isA<AccountWaits>().having((w) => w.wait, 'wait', wait)),
|
||||
);
|
||||
await settle();
|
||||
|
||||
expect(auth.status, Waiting(wait));
|
||||
expect(auth.usedSessions, isEmpty);
|
||||
expect(keys.kept, isEmpty);
|
||||
});
|
||||
|
||||
test('is refused alike for a wrong password or code, signing nobody in, '
|
||||
'and begins a new challenge for the next try', () async {
|
||||
final wrong = await passwords.unlock('anna@example.org', 'wrong');
|
||||
|
|
|
|||
|
|
@ -969,4 +969,45 @@ void main() {
|
|||
expect(anna.password, 'correct horse');
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets("an account waiting for the instance's operator signs in to "
|
||||
"its wait, which takes the sheet's place (Halkyon Learn's ADR 0021, "
|
||||
'D31)', (tester) async {
|
||||
final anna = dependencies.passwordGateway.addAccount(
|
||||
'anna@example.org',
|
||||
password: 'correct horse',
|
||||
crypto: dependencies.crypto,
|
||||
);
|
||||
dependencies.passwordGateway.accountWaits[anna.person] = fakeWaitOf(
|
||||
anna.person,
|
||||
handle: 'anna',
|
||||
).standing(AccountWaitState.suspended);
|
||||
await start(tester);
|
||||
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Sign in'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(field('Email address'), 'anna@example.org');
|
||||
await tester.enterText(field('Password'), 'correct horse');
|
||||
await tapButton(tester, 'Continue');
|
||||
await tester.enterText(
|
||||
field('Code from your authenticator app'),
|
||||
fakeTotpCode,
|
||||
);
|
||||
await tapButton(tester, 'Sign in');
|
||||
|
||||
expect(
|
||||
find.text(
|
||||
'The operator of test.example.org has suspended your account. Only '
|
||||
'they can lift it.',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('Code from your authenticator app'), findsNothing);
|
||||
expect(
|
||||
find.text("The address, the password or the code isn't right."),
|
||||
findsNothing,
|
||||
);
|
||||
expect(gateway.usedSessions, isEmpty);
|
||||
expect(dependencies.keys.kept, isEmpty);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue