SimpleLogin's sheet, its password and its recovery phrase both lost: under the lost-password step's phrase field, Lost your recovery phrase too? says what stays and what is lost and asks for new keys (`keys.startReset`) with the sign-in of a moment ago, which the device then drops. The next sign-in says when while it waits, and once due offers new keys, the sign-up's own password and phrase steps (`keys.completeReset`), or the password, which calls it off (D31). Every app's home shows `KeysResetBanner` where "too old" shows, and the account section's SimpleLogin row the same words (`KeysResetNotice`), each with Call it off, the password's proof alone (`keys.callOffResets`, D32), and, once due on a device that holds the keys, Finish here, which wraps the same keys anew (D33, D35). Both show only a reset of an account that signs in with SimpleLogin alone; ADR 0017's resets show in its own section, which now shares `AlertBox` with them. An account whose password signs in has no Forgot your password? there, and is asked its password and a code once while its keys lack the proof key, at a sign-in or on adding SimpleLogin (`password.setProofKey`, D34). `PasswordUnlock` carries the proof key, which its forgetting leaves, and the keys' password proves for a purpose given. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
618 lines
20 KiB
Dart
618 lines
20 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:clock/clock.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:halkyon_app/halkyon_app.dart';
|
|
import 'package:halkyon_app/testing.dart';
|
|
import 'package:halkyon_oidc_flutter/halkyon_oidc_flutter.dart';
|
|
|
|
/// A state at [step], [busy] or not, with [problem] or none.
|
|
Matcher at(SimpleLoginStep step, {bool busy = false, Exception? problem}) =>
|
|
isA<SimpleLoginState>()
|
|
.having((state) => state.step, 'step', step)
|
|
.having((state) => state.busy, 'busy', busy)
|
|
.having((state) => state.problem, 'problem', problem);
|
|
|
|
const password = 'correct horse battery';
|
|
|
|
void main() {
|
|
late FakeAuthGateway auth;
|
|
late FakeSimpleLoginGateway gateway;
|
|
late FakeOidcBrowser browser;
|
|
late FakeAccountCrypto crypto;
|
|
late MemoryAccountKeyStore keys;
|
|
late SimpleLoginRepository simpleLogin;
|
|
|
|
setUp(() {
|
|
auth = FakeAuthGateway(status: const SignedOut());
|
|
gateway = FakeSimpleLoginGateway(auth);
|
|
browser = FakeOidcBrowser();
|
|
crypto = FakeAccountCrypto();
|
|
keys = MemoryAccountKeyStore();
|
|
simpleLogin = SimpleLoginRepository(
|
|
gateway,
|
|
browser,
|
|
crypto,
|
|
keys,
|
|
auth,
|
|
callbackScheme: 'app.halkyon.test',
|
|
loopbackPort: 43823,
|
|
platform: TargetPlatform.android,
|
|
);
|
|
});
|
|
|
|
SimpleLoginCubit cubitFor({bool link = false}) {
|
|
final cubit = SimpleLoginCubit(simpleLogin, link: link);
|
|
addTearDown(cubit.close);
|
|
return cubit;
|
|
}
|
|
|
|
/// The words the check asks for, typed back right.
|
|
List<String> wordsAsked(SimpleLoginState state) => [
|
|
for (final position in state.checked) wordsOf(state.phrase)[position],
|
|
];
|
|
|
|
/// Makes the keys with [password] and types the phrase back.
|
|
Future<void> makeKeys(SimpleLoginCubit cubit) async {
|
|
await cubit.passwordEntered(password, password);
|
|
expect(cubit.state, at(SimpleLoginStep.phrase));
|
|
expect(cubit.state.phrase, fakePhrase);
|
|
cubit.phraseWrittenDown();
|
|
expect(cubit.state, at(SimpleLoginStep.phraseCheck));
|
|
await cubit.phraseChecked(wordsAsked(cubit.state));
|
|
}
|
|
|
|
group('someone new', () {
|
|
test('creates an account: the suggested handle checked, the password '
|
|
'twice, the phrase shown and three words typed back, signed in '
|
|
'only at the end', () async {
|
|
final cubit = cubitFor();
|
|
expect(cubit.state, at(SimpleLoginStep.browser));
|
|
|
|
await cubit.started();
|
|
expect(cubit.state, at(SimpleLoginStep.newcomer));
|
|
expect(cubit.state.handle, 'anna_mueller');
|
|
|
|
cubit.createChosen();
|
|
expect(cubit.state, at(SimpleLoginStep.handle));
|
|
expect(await cubit.checkHandle('anna_mueller'), isNull);
|
|
|
|
await cubit.handleEntered(' @anna_mueller ');
|
|
expect(cubit.state, at(SimpleLoginStep.password));
|
|
expect(cubit.state.handle, 'anna_mueller');
|
|
expect(
|
|
gateway.calls,
|
|
isNot(contains('checkHandle')),
|
|
reason: 'the instance suggests no handle anybody has',
|
|
);
|
|
|
|
await cubit.passwordEntered(password, 'correct horse batery');
|
|
expect(cubit.state.passwordsDiffer, isTrue);
|
|
await cubit.passwordEntered('too short', 'too short');
|
|
expect(
|
|
cubit.state,
|
|
at(
|
|
SimpleLoginStep.password,
|
|
problem: const PasswordFailed(PasswordProblem.passwordTooShort),
|
|
),
|
|
);
|
|
|
|
await cubit.passwordEntered(password, password);
|
|
cubit.phraseWrittenDown();
|
|
await cubit.phraseChecked(['wrong', 'words', 'typed']);
|
|
expect(cubit.state.phraseWrong, isTrue);
|
|
cubit.phraseShownAgain();
|
|
expect(cubit.state, at(SimpleLoginStep.phrase));
|
|
expect(gateway.calls, isNot(contains('signUp')));
|
|
expect(auth.status, const SignedOut());
|
|
|
|
cubit.phraseWrittenDown();
|
|
await cubit.phraseChecked(wordsAsked(cubit.state));
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.done));
|
|
expect(
|
|
auth.status,
|
|
const SignedIn(authUserId: 'person-1', isIdentified: true),
|
|
);
|
|
expect(gateway.accounts['person-1']!.handle, 'anna_mueller');
|
|
expect(keys.kept.keys.toList(), ['person-1']);
|
|
});
|
|
|
|
test('checks the handles typed, under the sign-in, each asked of the '
|
|
'instance (ADR 0020, D30), and goes on with one', () async {
|
|
gateway.addAccount('person-9', subject: 'other', handle: 'anna');
|
|
final cubit = cubitFor();
|
|
await cubit.started();
|
|
cubit.createChosen();
|
|
|
|
expect(await cubit.checkHandle('anna'), HandleProblem.taken);
|
|
expect(await cubit.checkHandle('admin'), HandleProblem.reserved);
|
|
expect(await cubit.checkHandle('a-b'), HandleProblem.invalid);
|
|
expect(await cubit.checkHandle('anna_m'), isNull);
|
|
expect(
|
|
gateway.calls.where((call) => call == 'checkHandle'),
|
|
hasLength(4),
|
|
);
|
|
|
|
await cubit.handleEntered(' ');
|
|
expect(
|
|
cubit.state,
|
|
at(
|
|
SimpleLoginStep.handle,
|
|
problem: const SimpleLoginFailed(SimpleLoginProblem.handleInvalid),
|
|
),
|
|
);
|
|
await cubit.handleEntered('anna_m');
|
|
expect(cubit.state, at(SimpleLoginStep.password));
|
|
expect(cubit.state.handle, 'anna_m');
|
|
});
|
|
|
|
test('goes back to the handle when it was taken meanwhile, then on with '
|
|
'the keys made, once changed', () async {
|
|
final cubit = cubitFor();
|
|
await cubit.started();
|
|
cubit.createChosen();
|
|
await cubit.handleEntered('anna_mueller');
|
|
gateway.addAccount('person-9', subject: 'other', handle: 'anna_mueller');
|
|
|
|
await makeKeys(cubit);
|
|
expect(
|
|
cubit.state,
|
|
at(
|
|
SimpleLoginStep.handle,
|
|
problem: const SimpleLoginFailed(SimpleLoginProblem.handleTaken),
|
|
),
|
|
);
|
|
|
|
await cubit.handleEntered('anna_m');
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.done));
|
|
final person = auth.usedSessions.single.authUserId;
|
|
expect(gateway.accounts[person]!.handle, 'anna_m');
|
|
});
|
|
|
|
test('links the account they have again, by its handle and recovery '
|
|
'phrase, refusing a phrase that does not fit', () async {
|
|
final material = await crypto.newKeys(password);
|
|
gateway.addAccount(
|
|
'person-9',
|
|
subject: 'simplelogin-lost',
|
|
handle: 'anna',
|
|
keys: material.setup,
|
|
);
|
|
final cubit = cubitFor();
|
|
await cubit.started();
|
|
|
|
cubit.linkChosen();
|
|
expect(cubit.state, at(SimpleLoginStep.mend));
|
|
await cubit.mendEntered('anna', otherFakePhrase);
|
|
expect(
|
|
cubit.state,
|
|
at(
|
|
SimpleLoginStep.mend,
|
|
problem: const PasswordFailed(PasswordProblem.phraseDoesNotFit),
|
|
),
|
|
);
|
|
cubit.choiceShownAgain();
|
|
expect(cubit.state, at(SimpleLoginStep.newcomer));
|
|
cubit.linkChosen();
|
|
|
|
await cubit.mendEntered('@anna', fakePhrase);
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.done));
|
|
expect(gateway.linked, {'simplelogin-anna': 'person-9'});
|
|
expect(keys.kept, {'person-9': material.keys});
|
|
});
|
|
|
|
test('begins again once their sign-in ran out', () async {
|
|
final cubit = cubitFor();
|
|
await cubit.started();
|
|
cubit.linkChosen();
|
|
gateway.failing = const SimpleLoginFailed(SimpleLoginProblem.startAgain);
|
|
|
|
await cubit.mendEntered('anna', fakePhrase);
|
|
expect(
|
|
cubit.state,
|
|
at(
|
|
SimpleLoginStep.browser,
|
|
problem: const SimpleLoginFailed(SimpleLoginProblem.startAgain),
|
|
),
|
|
);
|
|
|
|
gateway.failing = null;
|
|
await cubit.started();
|
|
expect(cubit.state, at(SimpleLoginStep.newcomer));
|
|
});
|
|
});
|
|
|
|
group('an account', () {
|
|
test('opens its keys with the password, refusing a wrong one', () async {
|
|
final material = await crypto.newKeys(password);
|
|
gateway.addAccount(
|
|
'person-9',
|
|
subject: 'simplelogin-anna',
|
|
handle: 'anna',
|
|
keys: material.setup,
|
|
);
|
|
final cubit = cubitFor();
|
|
await cubit.started();
|
|
expect(cubit.state, at(SimpleLoginStep.unlock));
|
|
expect(cubit.state.signedInAs, 'anna');
|
|
|
|
await cubit.unlockEntered('wrong horse battery');
|
|
expect(
|
|
cubit.state,
|
|
at(
|
|
SimpleLoginStep.unlock,
|
|
problem: const PasswordFailed(PasswordProblem.wrongCredentials),
|
|
),
|
|
);
|
|
expect(auth.status, const SignedOut());
|
|
|
|
await cubit.unlockEntered(password);
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.done));
|
|
expect(
|
|
auth.status,
|
|
const SignedIn(authUserId: 'person-9', isIdentified: true),
|
|
);
|
|
expect(keys.kept, {'person-9': material.keys});
|
|
});
|
|
|
|
test('with the password lost, opens them with the recovery phrase under '
|
|
'a new password typed twice, and goes back to the password when '
|
|
'asked', () async {
|
|
final material = await crypto.newKeys(password);
|
|
gateway.addAccount(
|
|
'person-9',
|
|
subject: 'simplelogin-anna',
|
|
handle: 'anna',
|
|
keys: material.setup,
|
|
);
|
|
final cubit = cubitFor();
|
|
await cubit.started();
|
|
cubit.forgotChosen();
|
|
expect(cubit.state, at(SimpleLoginStep.forgot));
|
|
cubit.unlockShownAgain();
|
|
expect(cubit.state, at(SimpleLoginStep.unlock));
|
|
cubit.forgotChosen();
|
|
|
|
const newPassword = 'a new horse battery';
|
|
await cubit.forgotEntered(fakePhrase, newPassword, 'a new horse');
|
|
expect(cubit.state, at(SimpleLoginStep.forgot));
|
|
expect(cubit.state.passwordsDiffer, isTrue);
|
|
expect(gateway.calls, isNot(contains('beginPhrase')));
|
|
|
|
await cubit.forgotEntered(otherFakePhrase, newPassword, newPassword);
|
|
expect(
|
|
cubit.state,
|
|
at(
|
|
SimpleLoginStep.forgot,
|
|
problem: const PasswordFailed(PasswordProblem.phraseDoesNotFit),
|
|
),
|
|
);
|
|
expect(cubit.state.passwordsDiffer, isFalse);
|
|
expect(auth.status, const SignedOut());
|
|
|
|
await cubit.forgotEntered(fakePhrase, newPassword, newPassword);
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.done));
|
|
expect(
|
|
auth.status,
|
|
const SignedIn(authUserId: 'person-9', isIdentified: true),
|
|
);
|
|
expect(keys.kept, {'person-9': material.keys});
|
|
});
|
|
|
|
test('of before, without keys, makes them with the handle it '
|
|
'lacks', () async {
|
|
gateway.addAccount('person-9', subject: 'simplelogin-anna');
|
|
final cubit = cubitFor();
|
|
await cubit.started();
|
|
expect(cubit.state, at(SimpleLoginStep.handle));
|
|
|
|
// Checked as the account it is, signed in with SimpleLogin.
|
|
expect(await cubit.checkHandle('anna'), isNull);
|
|
expect(gateway.calls, contains('checkAccountHandle'));
|
|
await cubit.handleEntered('anna');
|
|
expect(cubit.state, at(SimpleLoginStep.password));
|
|
expect(gateway.calls, isNot(contains('checkHandle')));
|
|
await makeKeys(cubit);
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.done));
|
|
expect(gateway.accounts['person-9']!.handle, 'anna');
|
|
expect(gateway.accounts['person-9']!.keys, isNotNull);
|
|
expect(auth.usedSessions.single.authUserId, 'person-9');
|
|
});
|
|
});
|
|
|
|
group('the password and the phrase lost: a reset of the keys (ADR 0020, '
|
|
'D31, D33)', () {
|
|
final asked = DateTime.utc(2026, 10, 24, 12);
|
|
final due = asked.add(const Duration(hours: 72));
|
|
late NewKeysMaterial material;
|
|
|
|
setUp(() async {
|
|
material = await crypto.newKeys(password);
|
|
gateway.addAccount(
|
|
'person-9',
|
|
subject: 'simplelogin-anna',
|
|
handle: 'anna',
|
|
keys: material.setup,
|
|
);
|
|
});
|
|
|
|
test('is asked for from the lost password, saying when it takes '
|
|
'effect; nobody is signed in meanwhile', () async {
|
|
await withClock(Clock.fixed(asked), () async {
|
|
final cubit = cubitFor();
|
|
await cubit.started();
|
|
cubit
|
|
..forgotChosen()
|
|
..phraseLost();
|
|
expect(cubit.state, at(SimpleLoginStep.resetAsk));
|
|
cubit.forgotChosen();
|
|
expect(cubit.state, at(SimpleLoginStep.forgot));
|
|
cubit.phraseLost();
|
|
|
|
await cubit.resetAsked();
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.resetAsked));
|
|
expect(cubit.state.resetAt, due);
|
|
expect(gateway.accounts['person-9']!.resetAt, due);
|
|
expect(auth.status, const SignedOut());
|
|
expect(keys.kept, isEmpty);
|
|
cubit.finished();
|
|
expect(cubit.state, at(SimpleLoginStep.closed));
|
|
});
|
|
});
|
|
|
|
test('waiting, the next sign-in says when, and the password opening the '
|
|
'keys calls it off', () async {
|
|
gateway.accounts['person-9']!.resetAt = due;
|
|
await withClock(Clock.fixed(asked), () async {
|
|
final cubit = cubitFor();
|
|
await cubit.started();
|
|
expect(cubit.state, at(SimpleLoginStep.unlock));
|
|
expect(cubit.state.resetAt, due);
|
|
|
|
await cubit.unlockEntered(password);
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.done));
|
|
expect(gateway.accounts['person-9']!.resetAt, isNull);
|
|
});
|
|
});
|
|
|
|
test('due, gives new keys under a new password and phrase, or opens the '
|
|
'old ones with the password', () async {
|
|
gateway.accounts['person-9']!.resetAt = due;
|
|
await withClock(Clock.fixed(due), () async {
|
|
final cubit = cubitFor();
|
|
await cubit.started();
|
|
expect(cubit.state, at(SimpleLoginStep.resetDue));
|
|
cubit.passwordChosen();
|
|
expect(cubit.state, at(SimpleLoginStep.unlock));
|
|
cubit.resetDueShownAgain();
|
|
expect(cubit.state, at(SimpleLoginStep.resetDue));
|
|
|
|
cubit.newKeysChosen();
|
|
expect(cubit.state, at(SimpleLoginStep.password));
|
|
expect(cubit.state.resetting, isTrue);
|
|
await makeKeys(cubit);
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.done));
|
|
expect(
|
|
auth.status,
|
|
const SignedIn(authUserId: 'person-9', isIdentified: true),
|
|
);
|
|
final account = gateway.accounts['person-9']!;
|
|
expect(account.keys, isNot(material.setup));
|
|
expect(account.resetAt, isNull);
|
|
expect(keys.kept['person-9'], isNot(material.keys));
|
|
});
|
|
});
|
|
|
|
test('due on a device that holds the keys, signed in, keeps them under '
|
|
'the new password and phrase (D33)', () async {
|
|
gateway.accounts['person-9']!.resetAt = due;
|
|
await auth.useSession(const SessionGrant('person-9', 'session-9'));
|
|
keys.kept['person-9'] = material.keys;
|
|
await withClock(Clock.fixed(due), () async {
|
|
final cubit = cubitFor(link: true);
|
|
await cubit.started();
|
|
expect(cubit.state, at(SimpleLoginStep.password));
|
|
expect(cubit.state.resetting, isTrue);
|
|
|
|
await makeKeys(cubit);
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.done));
|
|
expect(keys.kept, {'person-9': material.keys});
|
|
expect(gateway.accounts['person-9']!.resetAt, isNull);
|
|
expect(auth.usedSessions, hasLength(1), reason: 'no other session');
|
|
});
|
|
});
|
|
|
|
test('waiting on a device that holds the keys, adding SimpleLogin is done '
|
|
'at once', () async {
|
|
gateway.accounts['person-9']!.resetAt = due;
|
|
await auth.useSession(const SessionGrant('person-9', 'session-9'));
|
|
await withClock(Clock.fixed(asked), () async {
|
|
final cubit = cubitFor(link: true);
|
|
await cubit.started();
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.done));
|
|
expect(gateway.accounts['person-9']!.resetAt, due);
|
|
});
|
|
});
|
|
});
|
|
|
|
group('a password that signs in too (ADR 0020, D34)', () {
|
|
late NewKeysMaterial material;
|
|
|
|
setUp(() async {
|
|
material = await crypto.newKeys(password);
|
|
gateway.addAccount(
|
|
'person-9',
|
|
subject: 'simplelogin-anna',
|
|
handle: 'anna',
|
|
keys: material.setup,
|
|
passwordSigningIn: password,
|
|
proofKeyMissing: true,
|
|
);
|
|
});
|
|
|
|
test('opens the keys once it and a code set their proof key, a wrong '
|
|
'code refused', () async {
|
|
final cubit = cubitFor();
|
|
await cubit.started();
|
|
expect(cubit.state, at(SimpleLoginStep.proofKey));
|
|
expect(cubit.state.signedInAs, 'anna');
|
|
|
|
await cubit.proofKeyEntered(password, '000000');
|
|
expect(
|
|
cubit.state,
|
|
at(
|
|
SimpleLoginStep.proofKey,
|
|
problem: const PasswordFailed(PasswordProblem.wrongCode),
|
|
),
|
|
);
|
|
expect(auth.status, const SignedOut());
|
|
|
|
await cubit.proofKeyEntered(password, fakeTotpCode);
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.done));
|
|
expect(keys.kept, {'person-9': material.keys});
|
|
expect(gateway.accounts['person-9']!.proofKeyMissing, isFalse);
|
|
});
|
|
|
|
test('with the proof key, is asked alone, without the lost phrase, as its '
|
|
"resets are ADR 0017's", () async {
|
|
gateway.accounts['person-9']!.proofKeyMissing = false;
|
|
final cubit = cubitFor();
|
|
await cubit.started();
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.unlock));
|
|
expect(cubit.state.passwordSignsIn, isTrue);
|
|
});
|
|
|
|
test('adding SimpleLogin on a device signed in asks the same, and keeps '
|
|
'its session', () async {
|
|
await auth.useSession(const SessionGrant('person-9', 'session-9'));
|
|
keys.kept['person-9'] = material.keys;
|
|
final cubit = cubitFor(link: true);
|
|
await cubit.started();
|
|
expect(cubit.state, at(SimpleLoginStep.proofKey));
|
|
|
|
await cubit.proofKeyEntered(password, fakeTotpCode);
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.done));
|
|
expect(gateway.accounts['person-9']!.proofKeyMissing, isFalse);
|
|
expect(auth.usedSessions, hasLength(1), reason: 'no other session');
|
|
expect(keys.kept, {'person-9': material.keys});
|
|
});
|
|
});
|
|
|
|
group("SimpleLogin added to the account of the device's person", () {
|
|
test('a person without keys keeps those their device holds, and their '
|
|
'session, once the keys are set', () async {
|
|
await auth.switchToGitHub();
|
|
final deviceKeys = crypto.keysNumbered(1);
|
|
keys.kept['github-person'] = deviceKeys;
|
|
final cubit = cubitFor(link: true);
|
|
|
|
await cubit.started();
|
|
expect(cubit.state, at(SimpleLoginStep.handle));
|
|
await cubit.handleEntered('anna');
|
|
await makeKeys(cubit);
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.done));
|
|
expect(
|
|
auth.status,
|
|
const SignedIn(authUserId: 'github-person', isIdentified: true),
|
|
);
|
|
expect(keys.kept, {'github-person': deviceKeys});
|
|
expect(auth.usedSessions, isEmpty);
|
|
});
|
|
|
|
test('an account with keys is done at once', () async {
|
|
final material = await crypto.newKeys(password);
|
|
auth = FakeAuthGateway(
|
|
status: const SignedIn(authUserId: 'person-9', isIdentified: true),
|
|
);
|
|
gateway = FakeSimpleLoginGateway(auth);
|
|
gateway.accounts['person-9'] = FakeSimpleLoginAccount(
|
|
handle: 'anna',
|
|
keys: material.setup,
|
|
);
|
|
simpleLogin = SimpleLoginRepository(
|
|
gateway,
|
|
browser,
|
|
crypto,
|
|
keys,
|
|
auth,
|
|
callbackScheme: 'app.halkyon.test',
|
|
loopbackPort: 43823,
|
|
platform: TargetPlatform.android,
|
|
);
|
|
final cubit = cubitFor(link: true);
|
|
|
|
await cubit.started();
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.done));
|
|
expect(gateway.linked, {'simplelogin-anna': 'person-9'});
|
|
});
|
|
});
|
|
|
|
group('the browser', () {
|
|
test('closed, the sign-in ends with nothing done', () async {
|
|
browser.comesBack = () => null;
|
|
final cubit = cubitFor();
|
|
|
|
await cubit.started();
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.closed));
|
|
});
|
|
|
|
test("refused on SimpleLogin's page, says so and can begin "
|
|
'again', () async {
|
|
browser.comesBack = () => const OidcDenied('access_denied');
|
|
final cubit = cubitFor();
|
|
|
|
await cubit.started();
|
|
expect(
|
|
cubit.state,
|
|
at(
|
|
SimpleLoginStep.browser,
|
|
problem: const SimpleLoginFailed(SimpleLoginProblem.denied),
|
|
),
|
|
);
|
|
|
|
browser.comesBack = null;
|
|
await cubit.started();
|
|
expect(cubit.state, at(SimpleLoginStep.newcomer));
|
|
});
|
|
|
|
test('is no longer waited for once the sheet closes', () async {
|
|
final waiting = Completer<OidcCallback?>();
|
|
browser.comesBack = () => waiting.future;
|
|
final cubit = SimpleLoginCubit(simpleLogin);
|
|
|
|
unawaited(cubit.started());
|
|
await Future<void>.delayed(Duration.zero);
|
|
expect(cubit.state, at(SimpleLoginStep.browser, busy: true));
|
|
await cubit.close();
|
|
|
|
expect(browser.cancelled, 1);
|
|
waiting.complete(null);
|
|
});
|
|
|
|
test('is no longer waited for once cancelled', () async {
|
|
final cubit = cubitFor();
|
|
|
|
await cubit.cancelled();
|
|
|
|
expect(browser.cancelled, 1);
|
|
});
|
|
});
|
|
}
|