feat(app): signing in with SimpleLogin, its sheet and the start screen's offer (ADR 0020)
Where the instance offers SimpleLogin, the start screen offers it first,
saying that the instance keeps only the SimpleLogin ID, and opens a sheet
that waits for SimpleLogin's page in the browser (Cancel ends the wait),
then: for someone new, the choice between creating an account (the
suggested handle or another, checked with the instance, the password
twice, the recovery phrase shown once and three of its words typed back,
nothing reaching the instance before) and linking theirs again with its
handle and recovery phrase; for an account, its password opening its
keys; for an account of before, the keys made with the handle it lacks.
SimpleLoginCubit holds the steps, each refusal shown where it is mended
(a handle's on the handle, a sign-in to begin again on the browser's),
and closing the sheet stops waiting on the loopback.
The suggested handle is never checked: the instance suggests none that
anybody has, and every answer about a handle counts against its limit
(D5). The fake's handle rule is now the module's.
Learn and Todo build it with SimpleLogin's browser (halkyon_oidc_flutter,
now a direct dependency of both, with their names on the loopback's
page), their schemes and their loopback ports (43823, 43824), as the
instance's forwarding page has them. Their tests offer no SimpleLogin;
halkyon_app's test the sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 01:21:29 +02:00
|
|
|
import 'dart:async';
|
|
|
|
|
|
feat(app): a reset of the keys without the phrase, its banner, and the keys' proof key for a password that signs in (ADR 0020, D31–D35)
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>
2026-10-24 22:37:17 +02:00
|
|
|
import 'package:clock/clock.dart';
|
feat(app): signing in with SimpleLogin, its sheet and the start screen's offer (ADR 0020)
Where the instance offers SimpleLogin, the start screen offers it first,
saying that the instance keeps only the SimpleLogin ID, and opens a sheet
that waits for SimpleLogin's page in the browser (Cancel ends the wait),
then: for someone new, the choice between creating an account (the
suggested handle or another, checked with the instance, the password
twice, the recovery phrase shown once and three of its words typed back,
nothing reaching the instance before) and linking theirs again with its
handle and recovery phrase; for an account, its password opening its
keys; for an account of before, the keys made with the handle it lacks.
SimpleLoginCubit holds the steps, each refusal shown where it is mended
(a handle's on the handle, a sign-in to begin again on the browser's),
and closing the sheet stops waiting on the loopback.
The suggested handle is never checked: the instance suggests none that
anybody has, and every answer about a handle counts against its limit
(D5). The fake's handle rule is now the module's.
Learn and Todo build it with SimpleLogin's browser (halkyon_oidc_flutter,
now a direct dependency of both, with their names on the loopback's
page), their schemes and their loopback ports (43823, 43824), as the
instance's forwarding page has them. Their tests offer no SimpleLogin;
halkyon_app's test the sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 01:21:29 +02:00
|
|
|
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(() {
|
feat(app)!: no anonymous accounts on the device (ADR 0020, D4, D25)
Every account has a way in, so the apps make no anonymous account any
more. The start screen shows whenever nobody is signed in, a first
start included, and offers SimpleLogin, a password and GitHub for those
who have it, with no start without an account. An instance out of reach
is said so, with Try again, and one offering no way to create an account
too. An anonymous account's session left from before is deleted as the
app starts (D25), falling back to signing out on the device.
What only anonymous accounts needed goes: signing in anonymously, the
signed-out mark, adding GitHub to an account and switching to a GitHub
account from the Profile tab (with its Cancel, the only one), an
anonymous device's own keys, the warning that signing out deletes the
account, and the texts about keeping progress. Settings says on which
instance the account is signed in. Learn's publishing, reports and
suggestions and its learner count no longer ask whether the person has
an identity: everyone signed in has one.
The server still offers anonymous sign-in until the next commit, so
older apps keep working until v0.20.0 raises the minimum build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 14:50:55 +02:00
|
|
|
auth = FakeAuthGateway(status: const SignedOut());
|
feat(app): signing in with SimpleLogin, its sheet and the start screen's offer (ADR 0020)
Where the instance offers SimpleLogin, the start screen offers it first,
saying that the instance keeps only the SimpleLogin ID, and opens a sheet
that waits for SimpleLogin's page in the browser (Cancel ends the wait),
then: for someone new, the choice between creating an account (the
suggested handle or another, checked with the instance, the password
twice, the recovery phrase shown once and three of its words typed back,
nothing reaching the instance before) and linking theirs again with its
handle and recovery phrase; for an account, its password opening its
keys; for an account of before, the keys made with the handle it lacks.
SimpleLoginCubit holds the steps, each refusal shown where it is mended
(a handle's on the handle, a sign-in to begin again on the browser's),
and closing the sheet stops waiting on the loopback.
The suggested handle is never checked: the instance suggests none that
anybody has, and every answer about a handle counts against its limit
(D5). The fake's handle rule is now the module's.
Learn and Todo build it with SimpleLogin's browser (halkyon_oidc_flutter,
now a direct dependency of both, with their names on the loopback's
page), their schemes and their loopback ports (43823, 43824), as the
instance's forwarding page has them. Their tests offer no SimpleLogin;
halkyon_app's test the sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 01:21:29 +02:00
|
|
|
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));
|
feat(base)!: a handle at password sign-up (ADR 0020, D26, D28–D30)
Every new account gets a handle, and an account without one gets its
first as it sets a password. The handle is private until its person
goes public (D5).
- `passwordSignIn.signUp` takes a handle. `passwordSignIn.checkHandle`
says whether a sign-up could take one, only while that sign-up's
mailed code waits (`codeExpired` otherwise), counted per code request
(30 an hour) and per network (60) (D29). Members ask the register.
- `password.set` takes the first handle of an account without one
(D28); `SignInWays.handle` tells the app whether there is one.
- Each checks the handle before the code is used, so a refusal leaves
the code for another try. An existing account at the address keeps
its own handle where it has one, and the typed handle is still
checked, so the answer says nothing about the address.
- `first_handle.dart` holds what SimpleLogin, the keys and passwords
share: `refusalOfNewHandle`, `insertFirstProfile`, `hasHandle`.
- halkyon_app: one `HandleField` with its `HandleCheckCubit` (asked
300 ms after typing pauses, only about the handle typed last; the
instance's suggestion is free without asking) (D30). The password
setup asks for the handle between the phrase check and the code, and
a refusal at the code sends back to it. SimpleLogin's sign-up and
keys steps use the same field and now check as it is typed.
- The module's README and ADR 0020 (D29, D30, the built state) amended.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 18:09:04 +02:00
|
|
|
expect(await cubit.checkHandle('anna_mueller'), isNull);
|
feat(app): signing in with SimpleLogin, its sheet and the start screen's offer (ADR 0020)
Where the instance offers SimpleLogin, the start screen offers it first,
saying that the instance keeps only the SimpleLogin ID, and opens a sheet
that waits for SimpleLogin's page in the browser (Cancel ends the wait),
then: for someone new, the choice between creating an account (the
suggested handle or another, checked with the instance, the password
twice, the recovery phrase shown once and three of its words typed back,
nothing reaching the instance before) and linking theirs again with its
handle and recovery phrase; for an account, its password opening its
keys; for an account of before, the keys made with the handle it lacks.
SimpleLoginCubit holds the steps, each refusal shown where it is mended
(a handle's on the handle, a sign-in to begin again on the browser's),
and closing the sheet stops waiting on the loopback.
The suggested handle is never checked: the instance suggests none that
anybody has, and every answer about a handle counts against its limit
(D5). The fake's handle rule is now the module's.
Learn and Todo build it with SimpleLogin's browser (halkyon_oidc_flutter,
now a direct dependency of both, with their names on the loopback's
page), their schemes and their loopback ports (43823, 43824), as the
instance's forwarding page has them. Their tests offer no SimpleLogin;
halkyon_app's test the sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 01:21:29 +02:00
|
|
|
|
|
|
|
|
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']);
|
|
|
|
|
});
|
|
|
|
|
|
feat(base)!: a handle at password sign-up (ADR 0020, D26, D28–D30)
Every new account gets a handle, and an account without one gets its
first as it sets a password. The handle is private until its person
goes public (D5).
- `passwordSignIn.signUp` takes a handle. `passwordSignIn.checkHandle`
says whether a sign-up could take one, only while that sign-up's
mailed code waits (`codeExpired` otherwise), counted per code request
(30 an hour) and per network (60) (D29). Members ask the register.
- `password.set` takes the first handle of an account without one
(D28); `SignInWays.handle` tells the app whether there is one.
- Each checks the handle before the code is used, so a refusal leaves
the code for another try. An existing account at the address keeps
its own handle where it has one, and the typed handle is still
checked, so the answer says nothing about the address.
- `first_handle.dart` holds what SimpleLogin, the keys and passwords
share: `refusalOfNewHandle`, `insertFirstProfile`, `hasHandle`.
- halkyon_app: one `HandleField` with its `HandleCheckCubit` (asked
300 ms after typing pauses, only about the handle typed last; the
instance's suggestion is free without asking) (D30). The password
setup asks for the handle between the phrase check and the code, and
a refusal at the code sends back to it. SimpleLogin's sign-up and
keys steps use the same field and now check as it is typed.
- The module's README and ADR 0020 (D29, D30, the built state) amended.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 18:09:04 +02:00
|
|
|
test('checks the handles typed, under the sign-in, each asked of the '
|
|
|
|
|
'instance (ADR 0020, D30), and goes on with one', () async {
|
feat(app): signing in with SimpleLogin, its sheet and the start screen's offer (ADR 0020)
Where the instance offers SimpleLogin, the start screen offers it first,
saying that the instance keeps only the SimpleLogin ID, and opens a sheet
that waits for SimpleLogin's page in the browser (Cancel ends the wait),
then: for someone new, the choice between creating an account (the
suggested handle or another, checked with the instance, the password
twice, the recovery phrase shown once and three of its words typed back,
nothing reaching the instance before) and linking theirs again with its
handle and recovery phrase; for an account, its password opening its
keys; for an account of before, the keys made with the handle it lacks.
SimpleLoginCubit holds the steps, each refusal shown where it is mended
(a handle's on the handle, a sign-in to begin again on the browser's),
and closing the sheet stops waiting on the loopback.
The suggested handle is never checked: the instance suggests none that
anybody has, and every answer about a handle counts against its limit
(D5). The fake's handle rule is now the module's.
Learn and Todo build it with SimpleLogin's browser (halkyon_oidc_flutter,
now a direct dependency of both, with their names on the loopback's
page), their schemes and their loopback ports (43823, 43824), as the
instance's forwarding page has them. Their tests offer no SimpleLogin;
halkyon_app's test the sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 01:21:29 +02:00
|
|
|
gateway.addAccount('person-9', subject: 'other', handle: 'anna');
|
|
|
|
|
final cubit = cubitFor();
|
|
|
|
|
await cubit.started();
|
|
|
|
|
cubit.createChosen();
|
|
|
|
|
|
feat(base)!: a handle at password sign-up (ADR 0020, D26, D28–D30)
Every new account gets a handle, and an account without one gets its
first as it sets a password. The handle is private until its person
goes public (D5).
- `passwordSignIn.signUp` takes a handle. `passwordSignIn.checkHandle`
says whether a sign-up could take one, only while that sign-up's
mailed code waits (`codeExpired` otherwise), counted per code request
(30 an hour) and per network (60) (D29). Members ask the register.
- `password.set` takes the first handle of an account without one
(D28); `SignInWays.handle` tells the app whether there is one.
- Each checks the handle before the code is used, so a refusal leaves
the code for another try. An existing account at the address keeps
its own handle where it has one, and the typed handle is still
checked, so the answer says nothing about the address.
- `first_handle.dart` holds what SimpleLogin, the keys and passwords
share: `refusalOfNewHandle`, `insertFirstProfile`, `hasHandle`.
- halkyon_app: one `HandleField` with its `HandleCheckCubit` (asked
300 ms after typing pauses, only about the handle typed last; the
instance's suggestion is free without asking) (D30). The password
setup asks for the handle between the phrase check and the code, and
a refusal at the code sends back to it. SimpleLogin's sign-up and
keys steps use the same field and now check as it is typed.
- The module's README and ADR 0020 (D29, D30, the built state) amended.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 18:09:04 +02:00
|
|
|
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(' ');
|
feat(app): signing in with SimpleLogin, its sheet and the start screen's offer (ADR 0020)
Where the instance offers SimpleLogin, the start screen offers it first,
saying that the instance keeps only the SimpleLogin ID, and opens a sheet
that waits for SimpleLogin's page in the browser (Cancel ends the wait),
then: for someone new, the choice between creating an account (the
suggested handle or another, checked with the instance, the password
twice, the recovery phrase shown once and three of its words typed back,
nothing reaching the instance before) and linking theirs again with its
handle and recovery phrase; for an account, its password opening its
keys; for an account of before, the keys made with the handle it lacks.
SimpleLoginCubit holds the steps, each refusal shown where it is mended
(a handle's on the handle, a sign-in to begin again on the browser's),
and closing the sheet stops waiting on the loopback.
The suggested handle is never checked: the instance suggests none that
anybody has, and every answer about a handle counts against its limit
(D5). The fake's handle rule is now the module's.
Learn and Todo build it with SimpleLogin's browser (halkyon_oidc_flutter,
now a direct dependency of both, with their names on the loopback's
page), their schemes and their loopback ports (43823, 43824), as the
instance's forwarding page has them. Their tests offer no SimpleLogin;
halkyon_app's test the sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 01:21:29 +02:00
|
|
|
expect(
|
|
|
|
|
cubit.state,
|
|
|
|
|
at(
|
|
|
|
|
SimpleLoginStep.handle,
|
feat(base)!: a handle at password sign-up (ADR 0020, D26, D28–D30)
Every new account gets a handle, and an account without one gets its
first as it sets a password. The handle is private until its person
goes public (D5).
- `passwordSignIn.signUp` takes a handle. `passwordSignIn.checkHandle`
says whether a sign-up could take one, only while that sign-up's
mailed code waits (`codeExpired` otherwise), counted per code request
(30 an hour) and per network (60) (D29). Members ask the register.
- `password.set` takes the first handle of an account without one
(D28); `SignInWays.handle` tells the app whether there is one.
- Each checks the handle before the code is used, so a refusal leaves
the code for another try. An existing account at the address keeps
its own handle where it has one, and the typed handle is still
checked, so the answer says nothing about the address.
- `first_handle.dart` holds what SimpleLogin, the keys and passwords
share: `refusalOfNewHandle`, `insertFirstProfile`, `hasHandle`.
- halkyon_app: one `HandleField` with its `HandleCheckCubit` (asked
300 ms after typing pauses, only about the handle typed last; the
instance's suggestion is free without asking) (D30). The password
setup asks for the handle between the phrase check and the code, and
a refusal at the code sends back to it. SimpleLogin's sign-up and
keys steps use the same field and now check as it is typed.
- The module's README and ADR 0020 (D29, D30, the built state) amended.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 18:09:04 +02:00
|
|
|
problem: const SimpleLoginFailed(SimpleLoginProblem.handleInvalid),
|
feat(app): signing in with SimpleLogin, its sheet and the start screen's offer (ADR 0020)
Where the instance offers SimpleLogin, the start screen offers it first,
saying that the instance keeps only the SimpleLogin ID, and opens a sheet
that waits for SimpleLogin's page in the browser (Cancel ends the wait),
then: for someone new, the choice between creating an account (the
suggested handle or another, checked with the instance, the password
twice, the recovery phrase shown once and three of its words typed back,
nothing reaching the instance before) and linking theirs again with its
handle and recovery phrase; for an account, its password opening its
keys; for an account of before, the keys made with the handle it lacks.
SimpleLoginCubit holds the steps, each refusal shown where it is mended
(a handle's on the handle, a sign-in to begin again on the browser's),
and closing the sheet stops waiting on the loopback.
The suggested handle is never checked: the instance suggests none that
anybody has, and every answer about a handle counts against its limit
(D5). The fake's handle rule is now the module's.
Learn and Todo build it with SimpleLogin's browser (halkyon_oidc_flutter,
now a direct dependency of both, with their names on the loopback's
page), their schemes and their loopback ports (43823, 43824), as the
instance's forwarding page has them. Their tests offer no SimpleLogin;
halkyon_app's test the sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 01:21:29 +02:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
await cubit.handleEntered('anna_m');
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.password));
|
feat(base)!: a handle at password sign-up (ADR 0020, D26, D28–D30)
Every new account gets a handle, and an account without one gets its
first as it sets a password. The handle is private until its person
goes public (D5).
- `passwordSignIn.signUp` takes a handle. `passwordSignIn.checkHandle`
says whether a sign-up could take one, only while that sign-up's
mailed code waits (`codeExpired` otherwise), counted per code request
(30 an hour) and per network (60) (D29). Members ask the register.
- `password.set` takes the first handle of an account without one
(D28); `SignInWays.handle` tells the app whether there is one.
- Each checks the handle before the code is used, so a refusal leaves
the code for another try. An existing account at the address keeps
its own handle where it has one, and the typed handle is still
checked, so the answer says nothing about the address.
- `first_handle.dart` holds what SimpleLogin, the keys and passwords
share: `refusalOfNewHandle`, `insertFirstProfile`, `hasHandle`.
- halkyon_app: one `HandleField` with its `HandleCheckCubit` (asked
300 ms after typing pauses, only about the handle typed last; the
instance's suggestion is free without asking) (D30). The password
setup asks for the handle between the phrase check and the code, and
a refusal at the code sends back to it. SimpleLogin's sign-up and
keys steps use the same field and now check as it is typed.
- The module's README and ADR 0020 (D29, D30, the built state) amended.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 18:09:04 +02:00
|
|
|
expect(cubit.state.handle, 'anna_m');
|
feat(app): signing in with SimpleLogin, its sheet and the start screen's offer (ADR 0020)
Where the instance offers SimpleLogin, the start screen offers it first,
saying that the instance keeps only the SimpleLogin ID, and opens a sheet
that waits for SimpleLogin's page in the browser (Cancel ends the wait),
then: for someone new, the choice between creating an account (the
suggested handle or another, checked with the instance, the password
twice, the recovery phrase shown once and three of its words typed back,
nothing reaching the instance before) and linking theirs again with its
handle and recovery phrase; for an account, its password opening its
keys; for an account of before, the keys made with the handle it lacks.
SimpleLoginCubit holds the steps, each refusal shown where it is mended
(a handle's on the handle, a sign-in to begin again on the browser's),
and closing the sheet stops waiting on the loopback.
The suggested handle is never checked: the instance suggests none that
anybody has, and every answer about a handle counts against its limit
(D5). The fake's handle rule is now the module's.
Learn and Todo build it with SimpleLogin's browser (halkyon_oidc_flutter,
now a direct dependency of both, with their names on the loopback's
page), their schemes and their loopback ports (43823, 43824), as the
instance's forwarding page has them. Their tests offer no SimpleLogin;
halkyon_app's test the sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 01:21:29 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
feat(app): a lost password with the recovery phrase in SimpleLogin's sheet (ADR 0020, 7)
An account that signs in with SimpleLogin opens its keys with its
password. Where that is lost, the unlock step's "Forgot your password?"
opens one step: the recovery phrase, and a new password twice. There is
no address and no code: the SimpleLogin sign-in of a moment ago stands
for them (D20).
- The device opens the keys with the phrase (`keys.beginPhrase`) and
wraps them under the new password (`AccountCrypto.newKeysPassword`).
It then signs the phrase's proof bound to that password
(`signKeysResetProof`), which `keys.resetPassword` takes with the
sign-in. The keys stay, and every other session ends.
- A phrase that doesn't open the keys is refused on the device, and the
instance gets no proof to count. Two passwords that differ are
refused before anything is sent.
- The gateway's `beginPhrase` and `resetPassword` run on the account's
own session, not yet the device's. The repository's `resetPassword`
ties them together, and the cubit and sheet have their `forgot` step.
Three texts are new, in English and German.
- halkyon_base_server exports `isKeysPassword` for the interop test,
which checks against the register's checks, with real libsodium, what
the device sends.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 21:25:47 +02:00
|
|
|
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);
|
|
|
|
|
|
feat(app): signing in with SimpleLogin, its sheet and the start screen's offer (ADR 0020)
Where the instance offers SimpleLogin, the start screen offers it first,
saying that the instance keeps only the SimpleLogin ID, and opens a sheet
that waits for SimpleLogin's page in the browser (Cancel ends the wait),
then: for someone new, the choice between creating an account (the
suggested handle or another, checked with the instance, the password
twice, the recovery phrase shown once and three of its words typed back,
nothing reaching the instance before) and linking theirs again with its
handle and recovery phrase; for an account, its password opening its
keys; for an account of before, the keys made with the handle it lacks.
SimpleLoginCubit holds the steps, each refusal shown where it is mended
(a handle's on the handle, a sign-in to begin again on the browser's),
and closing the sheet stops waiting on the loopback.
The suggested handle is never checked: the instance suggests none that
anybody has, and every answer about a handle counts against its limit
(D5). The fake's handle rule is now the module's.
Learn and Todo build it with SimpleLogin's browser (halkyon_oidc_flutter,
now a direct dependency of both, with their names on the loopback's
page), their schemes and their loopback ports (43823, 43824), as the
instance's forwarding page has them. Their tests offer no SimpleLogin;
halkyon_app's test the sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 01:21:29 +02:00
|
|
|
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));
|
|
|
|
|
|
feat(base)!: a handle at password sign-up (ADR 0020, D26, D28–D30)
Every new account gets a handle, and an account without one gets its
first as it sets a password. The handle is private until its person
goes public (D5).
- `passwordSignIn.signUp` takes a handle. `passwordSignIn.checkHandle`
says whether a sign-up could take one, only while that sign-up's
mailed code waits (`codeExpired` otherwise), counted per code request
(30 an hour) and per network (60) (D29). Members ask the register.
- `password.set` takes the first handle of an account without one
(D28); `SignInWays.handle` tells the app whether there is one.
- Each checks the handle before the code is used, so a refusal leaves
the code for another try. An existing account at the address keeps
its own handle where it has one, and the typed handle is still
checked, so the answer says nothing about the address.
- `first_handle.dart` holds what SimpleLogin, the keys and passwords
share: `refusalOfNewHandle`, `insertFirstProfile`, `hasHandle`.
- halkyon_app: one `HandleField` with its `HandleCheckCubit` (asked
300 ms after typing pauses, only about the handle typed last; the
instance's suggestion is free without asking) (D30). The password
setup asks for the handle between the phrase check and the code, and
a refusal at the code sends back to it. SimpleLogin's sign-up and
keys steps use the same field and now check as it is typed.
- The module's README and ADR 0020 (D29, D30, the built state) amended.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 18:09:04 +02:00
|
|
|
// Checked as the account it is, signed in with SimpleLogin.
|
|
|
|
|
expect(await cubit.checkHandle('anna'), isNull);
|
|
|
|
|
expect(gateway.calls, contains('checkAccountHandle'));
|
feat(app): signing in with SimpleLogin, its sheet and the start screen's offer (ADR 0020)
Where the instance offers SimpleLogin, the start screen offers it first,
saying that the instance keeps only the SimpleLogin ID, and opens a sheet
that waits for SimpleLogin's page in the browser (Cancel ends the wait),
then: for someone new, the choice between creating an account (the
suggested handle or another, checked with the instance, the password
twice, the recovery phrase shown once and three of its words typed back,
nothing reaching the instance before) and linking theirs again with its
handle and recovery phrase; for an account, its password opening its
keys; for an account of before, the keys made with the handle it lacks.
SimpleLoginCubit holds the steps, each refusal shown where it is mended
(a handle's on the handle, a sign-in to begin again on the browser's),
and closing the sheet stops waiting on the loopback.
The suggested handle is never checked: the instance suggests none that
anybody has, and every answer about a handle counts against its limit
(D5). The fake's handle rule is now the module's.
Learn and Todo build it with SimpleLogin's browser (halkyon_oidc_flutter,
now a direct dependency of both, with their names on the loopback's
page), their schemes and their loopback ports (43823, 43824), as the
instance's forwarding page has them. Their tests offer no SimpleLogin;
halkyon_app's test the sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 01:21:29 +02:00
|
|
|
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');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
feat(app): a reset of the keys without the phrase, its banner, and the keys' proof key for a password that signs in (ADR 0020, D31–D35)
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>
2026-10-24 22:37:17 +02:00
|
|
|
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});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
feat(app): signing in with SimpleLogin, its sheet and the start screen's offer (ADR 0020)
Where the instance offers SimpleLogin, the start screen offers it first,
saying that the instance keeps only the SimpleLogin ID, and opens a sheet
that waits for SimpleLogin's page in the browser (Cancel ends the wait),
then: for someone new, the choice between creating an account (the
suggested handle or another, checked with the instance, the password
twice, the recovery phrase shown once and three of its words typed back,
nothing reaching the instance before) and linking theirs again with its
handle and recovery phrase; for an account, its password opening its
keys; for an account of before, the keys made with the handle it lacks.
SimpleLoginCubit holds the steps, each refusal shown where it is mended
(a handle's on the handle, a sign-in to begin again on the browser's),
and closing the sheet stops waiting on the loopback.
The suggested handle is never checked: the instance suggests none that
anybody has, and every answer about a handle counts against its limit
(D5). The fake's handle rule is now the module's.
Learn and Todo build it with SimpleLogin's browser (halkyon_oidc_flutter,
now a direct dependency of both, with their names on the loopback's
page), their schemes and their loopback ports (43823, 43824), as the
instance's forwarding page has them. Their tests offer no SimpleLogin;
halkyon_app's test the sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 01:21:29 +02:00
|
|
|
group("SimpleLogin added to the account of the device's person", () {
|
feat(app)!: no anonymous accounts on the device (ADR 0020, D4, D25)
Every account has a way in, so the apps make no anonymous account any
more. The start screen shows whenever nobody is signed in, a first
start included, and offers SimpleLogin, a password and GitHub for those
who have it, with no start without an account. An instance out of reach
is said so, with Try again, and one offering no way to create an account
too. An anonymous account's session left from before is deleted as the
app starts (D25), falling back to signing out on the device.
What only anonymous accounts needed goes: signing in anonymously, the
signed-out mark, adding GitHub to an account and switching to a GitHub
account from the Profile tab (with its Cancel, the only one), an
anonymous device's own keys, the warning that signing out deletes the
account, and the texts about keeping progress. Settings says on which
instance the account is signed in. Learn's publishing, reports and
suggestions and its learner count no longer ask whether the person has
an identity: everyone signed in has one.
The server still offers anonymous sign-in until the next commit, so
older apps keep working until v0.20.0 raises the minimum build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 14:50:55 +02:00
|
|
|
test('a person without keys keeps those their device holds, and their '
|
|
|
|
|
'session, once the keys are set', () async {
|
feat(base,app)!: GitHub signs nobody in any more (ADR 0020, D36)
Every account signs in with SimpleLogin or a password now, so GitHub
goes from the module, the shared app package, Learn and Todo, ahead of
ADR 0017's GITHUB_SIGN_IN_ENDS.
- halkyon_base: no `gitHubSignIn` endpoint, GitHub identity provider or
GitHub refusals; no `githubClientId`/`githubClientSecret`, no
`GITHUB_SIGN_IN_ENDS` and no `forgetGitHubAccounts`; the register
loses `account-with-github`. `SignInWays.gitHub`, the basics'
`gitHubClientId` and `gitHubSignInEnds`, and `PasswordRefusal`'s
`signInAgain` and `noAddress` go. A first password takes a code
mailed to the person's own address; an account without one sets none
(`wrongCredentials`).
- Learn's server: `instance.info()` without `gitHubClientId`, the
pruning without GitHub, and `minimumAppBuild` 24, as builds up to 23
(v0.20.0) require `SignInWays.gitHub`: the server deploys with the
release of build 24.
- halkyon_app: no GitHub button or problems on the start screen, no
password offered after a GitHub sign-in, and `ServerpodAuthGateway`
without the browser half only GitHub used (`(client, base:)`). The
account section offers a first password only to an account with an
address. `LoopbackWebAuth` and the apps' schemes stay, SimpleLogin's
ways back.
- Learn's Profile lists no GitHub; the texts go (English, German).
- Serverpod's `serverpod_auth_idp_github_account` stays, as its
migrations make it; a row goes with its account, and ADR 0020's D36
gives operators the SQL to delete those left.
- deploy/: compose.yaml and .env.example without GitHub. The READMEs,
AGENTS.md files and ADR 0017 amended.
BREAKING CHANGE: builds up to 23 can't read the instance's answers
about an account's ways in; minimumAppBuild is 24.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-25 12:52:52 +01:00
|
|
|
auth.signIn('ada');
|
feat(app): signing in with SimpleLogin, its sheet and the start screen's offer (ADR 0020)
Where the instance offers SimpleLogin, the start screen offers it first,
saying that the instance keeps only the SimpleLogin ID, and opens a sheet
that waits for SimpleLogin's page in the browser (Cancel ends the wait),
then: for someone new, the choice between creating an account (the
suggested handle or another, checked with the instance, the password
twice, the recovery phrase shown once and three of its words typed back,
nothing reaching the instance before) and linking theirs again with its
handle and recovery phrase; for an account, its password opening its
keys; for an account of before, the keys made with the handle it lacks.
SimpleLoginCubit holds the steps, each refusal shown where it is mended
(a handle's on the handle, a sign-in to begin again on the browser's),
and closing the sheet stops waiting on the loopback.
The suggested handle is never checked: the instance suggests none that
anybody has, and every answer about a handle counts against its limit
(D5). The fake's handle rule is now the module's.
Learn and Todo build it with SimpleLogin's browser (halkyon_oidc_flutter,
now a direct dependency of both, with their names on the loopback's
page), their schemes and their loopback ports (43823, 43824), as the
instance's forwarding page has them. Their tests offer no SimpleLogin;
halkyon_app's test the sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 01:21:29 +02:00
|
|
|
final deviceKeys = crypto.keysNumbered(1);
|
feat(base,app)!: GitHub signs nobody in any more (ADR 0020, D36)
Every account signs in with SimpleLogin or a password now, so GitHub
goes from the module, the shared app package, Learn and Todo, ahead of
ADR 0017's GITHUB_SIGN_IN_ENDS.
- halkyon_base: no `gitHubSignIn` endpoint, GitHub identity provider or
GitHub refusals; no `githubClientId`/`githubClientSecret`, no
`GITHUB_SIGN_IN_ENDS` and no `forgetGitHubAccounts`; the register
loses `account-with-github`. `SignInWays.gitHub`, the basics'
`gitHubClientId` and `gitHubSignInEnds`, and `PasswordRefusal`'s
`signInAgain` and `noAddress` go. A first password takes a code
mailed to the person's own address; an account without one sets none
(`wrongCredentials`).
- Learn's server: `instance.info()` without `gitHubClientId`, the
pruning without GitHub, and `minimumAppBuild` 24, as builds up to 23
(v0.20.0) require `SignInWays.gitHub`: the server deploys with the
release of build 24.
- halkyon_app: no GitHub button or problems on the start screen, no
password offered after a GitHub sign-in, and `ServerpodAuthGateway`
without the browser half only GitHub used (`(client, base:)`). The
account section offers a first password only to an account with an
address. `LoopbackWebAuth` and the apps' schemes stay, SimpleLogin's
ways back.
- Learn's Profile lists no GitHub; the texts go (English, German).
- Serverpod's `serverpod_auth_idp_github_account` stays, as its
migrations make it; a row goes with its account, and ADR 0020's D36
gives operators the SQL to delete those left.
- deploy/: compose.yaml and .env.example without GitHub. The READMEs,
AGENTS.md files and ADR 0017 amended.
BREAKING CHANGE: builds up to 23 can't read the instance's answers
about an account's ways in; minimumAppBuild is 24.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-25 12:52:52 +01:00
|
|
|
keys.kept['ada'] = deviceKeys;
|
feat(app): signing in with SimpleLogin, its sheet and the start screen's offer (ADR 0020)
Where the instance offers SimpleLogin, the start screen offers it first,
saying that the instance keeps only the SimpleLogin ID, and opens a sheet
that waits for SimpleLogin's page in the browser (Cancel ends the wait),
then: for someone new, the choice between creating an account (the
suggested handle or another, checked with the instance, the password
twice, the recovery phrase shown once and three of its words typed back,
nothing reaching the instance before) and linking theirs again with its
handle and recovery phrase; for an account, its password opening its
keys; for an account of before, the keys made with the handle it lacks.
SimpleLoginCubit holds the steps, each refusal shown where it is mended
(a handle's on the handle, a sign-in to begin again on the browser's),
and closing the sheet stops waiting on the loopback.
The suggested handle is never checked: the instance suggests none that
anybody has, and every answer about a handle counts against its limit
(D5). The fake's handle rule is now the module's.
Learn and Todo build it with SimpleLogin's browser (halkyon_oidc_flutter,
now a direct dependency of both, with their names on the loopback's
page), their schemes and their loopback ports (43823, 43824), as the
instance's forwarding page has them. Their tests offer no SimpleLogin;
halkyon_app's test the sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 01:21:29 +02:00
|
|
|
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,
|
feat(base,app)!: GitHub signs nobody in any more (ADR 0020, D36)
Every account signs in with SimpleLogin or a password now, so GitHub
goes from the module, the shared app package, Learn and Todo, ahead of
ADR 0017's GITHUB_SIGN_IN_ENDS.
- halkyon_base: no `gitHubSignIn` endpoint, GitHub identity provider or
GitHub refusals; no `githubClientId`/`githubClientSecret`, no
`GITHUB_SIGN_IN_ENDS` and no `forgetGitHubAccounts`; the register
loses `account-with-github`. `SignInWays.gitHub`, the basics'
`gitHubClientId` and `gitHubSignInEnds`, and `PasswordRefusal`'s
`signInAgain` and `noAddress` go. A first password takes a code
mailed to the person's own address; an account without one sets none
(`wrongCredentials`).
- Learn's server: `instance.info()` without `gitHubClientId`, the
pruning without GitHub, and `minimumAppBuild` 24, as builds up to 23
(v0.20.0) require `SignInWays.gitHub`: the server deploys with the
release of build 24.
- halkyon_app: no GitHub button or problems on the start screen, no
password offered after a GitHub sign-in, and `ServerpodAuthGateway`
without the browser half only GitHub used (`(client, base:)`). The
account section offers a first password only to an account with an
address. `LoopbackWebAuth` and the apps' schemes stay, SimpleLogin's
ways back.
- Learn's Profile lists no GitHub; the texts go (English, German).
- Serverpod's `serverpod_auth_idp_github_account` stays, as its
migrations make it; a row goes with its account, and ADR 0020's D36
gives operators the SQL to delete those left.
- deploy/: compose.yaml and .env.example without GitHub. The READMEs,
AGENTS.md files and ADR 0017 amended.
BREAKING CHANGE: builds up to 23 can't read the instance's answers
about an account's ways in; minimumAppBuild is 24.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-25 12:52:52 +01:00
|
|
|
const SignedIn(authUserId: 'ada', isIdentified: true),
|
feat(app): signing in with SimpleLogin, its sheet and the start screen's offer (ADR 0020)
Where the instance offers SimpleLogin, the start screen offers it first,
saying that the instance keeps only the SimpleLogin ID, and opens a sheet
that waits for SimpleLogin's page in the browser (Cancel ends the wait),
then: for someone new, the choice between creating an account (the
suggested handle or another, checked with the instance, the password
twice, the recovery phrase shown once and three of its words typed back,
nothing reaching the instance before) and linking theirs again with its
handle and recovery phrase; for an account, its password opening its
keys; for an account of before, the keys made with the handle it lacks.
SimpleLoginCubit holds the steps, each refusal shown where it is mended
(a handle's on the handle, a sign-in to begin again on the browser's),
and closing the sheet stops waiting on the loopback.
The suggested handle is never checked: the instance suggests none that
anybody has, and every answer about a handle counts against its limit
(D5). The fake's handle rule is now the module's.
Learn and Todo build it with SimpleLogin's browser (halkyon_oidc_flutter,
now a direct dependency of both, with their names on the loopback's
page), their schemes and their loopback ports (43823, 43824), as the
instance's forwarding page has them. Their tests offer no SimpleLogin;
halkyon_app's test the sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 01:21:29 +02:00
|
|
|
);
|
feat(base,app)!: GitHub signs nobody in any more (ADR 0020, D36)
Every account signs in with SimpleLogin or a password now, so GitHub
goes from the module, the shared app package, Learn and Todo, ahead of
ADR 0017's GITHUB_SIGN_IN_ENDS.
- halkyon_base: no `gitHubSignIn` endpoint, GitHub identity provider or
GitHub refusals; no `githubClientId`/`githubClientSecret`, no
`GITHUB_SIGN_IN_ENDS` and no `forgetGitHubAccounts`; the register
loses `account-with-github`. `SignInWays.gitHub`, the basics'
`gitHubClientId` and `gitHubSignInEnds`, and `PasswordRefusal`'s
`signInAgain` and `noAddress` go. A first password takes a code
mailed to the person's own address; an account without one sets none
(`wrongCredentials`).
- Learn's server: `instance.info()` without `gitHubClientId`, the
pruning without GitHub, and `minimumAppBuild` 24, as builds up to 23
(v0.20.0) require `SignInWays.gitHub`: the server deploys with the
release of build 24.
- halkyon_app: no GitHub button or problems on the start screen, no
password offered after a GitHub sign-in, and `ServerpodAuthGateway`
without the browser half only GitHub used (`(client, base:)`). The
account section offers a first password only to an account with an
address. `LoopbackWebAuth` and the apps' schemes stay, SimpleLogin's
ways back.
- Learn's Profile lists no GitHub; the texts go (English, German).
- Serverpod's `serverpod_auth_idp_github_account` stays, as its
migrations make it; a row goes with its account, and ADR 0020's D36
gives operators the SQL to delete those left.
- deploy/: compose.yaml and .env.example without GitHub. The READMEs,
AGENTS.md files and ADR 0017 amended.
BREAKING CHANGE: builds up to 23 can't read the instance's answers
about an account's ways in; minimumAppBuild is 24.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-25 12:52:52 +01:00
|
|
|
expect(keys.kept, {'ada': deviceKeys});
|
feat(app): signing in with SimpleLogin, its sheet and the start screen's offer (ADR 0020)
Where the instance offers SimpleLogin, the start screen offers it first,
saying that the instance keeps only the SimpleLogin ID, and opens a sheet
that waits for SimpleLogin's page in the browser (Cancel ends the wait),
then: for someone new, the choice between creating an account (the
suggested handle or another, checked with the instance, the password
twice, the recovery phrase shown once and three of its words typed back,
nothing reaching the instance before) and linking theirs again with its
handle and recovery phrase; for an account, its password opening its
keys; for an account of before, the keys made with the handle it lacks.
SimpleLoginCubit holds the steps, each refusal shown where it is mended
(a handle's on the handle, a sign-in to begin again on the browser's),
and closing the sheet stops waiting on the loopback.
The suggested handle is never checked: the instance suggests none that
anybody has, and every answer about a handle counts against its limit
(D5). The fake's handle rule is now the module's.
Learn and Todo build it with SimpleLogin's browser (halkyon_oidc_flutter,
now a direct dependency of both, with their names on the loopback's
page), their schemes and their loopback ports (43823, 43824), as the
instance's forwarding page has them. Their tests offer no SimpleLogin;
halkyon_app's test the sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 01:21:29 +02:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|