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_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';
|
|
|
|
|
import 'package:material_ui/material_ui.dart';
|
|
|
|
|
|
|
|
|
|
/// A signed-in person's app as a test makes one: whose it is.
|
|
|
|
|
class const _PersonApp(final SignedIn person) extends StatelessWidget {
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) => MaterialApp(
|
|
|
|
|
home: Scaffold(body: Text('App of ${person.authUserId}')),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const password = 'correct horse battery';
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
late FakeAuthGateway gateway;
|
|
|
|
|
late TestAppDependencies dependencies;
|
|
|
|
|
|
|
|
|
|
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
|
|
|
gateway = 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
|
|
|
dependencies = TestAppDependencies(gateway: gateway);
|
|
|
|
|
dependencies.simpleLoginGateway.isOffered = true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/// Starts signed out, on a phone tall enough for every sheet.
|
|
|
|
|
Future<void> start(WidgetTester tester) async {
|
|
|
|
|
tester.view.physicalSize = const Size(412, 1800);
|
|
|
|
|
tester.view.devicePixelRatio = 1;
|
|
|
|
|
addTearDown(tester.view.reset);
|
|
|
|
|
await tester.pumpWidget(
|
|
|
|
|
AppFrameScope(
|
|
|
|
|
frame: testFrame(),
|
|
|
|
|
child: AppScope(
|
|
|
|
|
dependencies: dependencies,
|
|
|
|
|
child: SessionGate(
|
|
|
|
|
look: (context) => (themeMode: ThemeMode.light, locale: null),
|
|
|
|
|
app: _PersonApp.new,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Finder field(String label) => find.widgetWithText(TextField, label);
|
|
|
|
|
|
|
|
|
|
Future<void> tapButton(WidgetTester tester, String label) async {
|
|
|
|
|
final button = find.widgetWithText(FilledButton, label).last;
|
|
|
|
|
await tester.ensureVisible(button);
|
|
|
|
|
await tester.tap(button);
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Types the recovery phrase's words the check asks for.
|
|
|
|
|
Future<void> typeCheckedWords(WidgetTester tester, String phrase) async {
|
|
|
|
|
final words = wordsOf(phrase);
|
|
|
|
|
for (final element in find.byType(TextField).evaluate()) {
|
|
|
|
|
if ((element.widget as TextField).decoration?.labelText case final label?
|
|
|
|
|
when label.startsWith('Word ')) {
|
|
|
|
|
await tester.enterText(
|
|
|
|
|
field(label),
|
|
|
|
|
words[int.parse(label.substring('Word '.length)) - 1],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
testWidgets('the start screen offers SimpleLogin first, saying what it '
|
|
|
|
|
'learns and what the instance keeps', (tester) async {
|
|
|
|
|
await start(tester);
|
|
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
find.widgetWithText(FilledButton, 'Continue with SimpleLogin'),
|
|
|
|
|
findsOneWidget,
|
|
|
|
|
);
|
|
|
|
|
expect(
|
|
|
|
|
find.textContaining('keeps only your SimpleLogin ID'),
|
|
|
|
|
findsOneWidget,
|
|
|
|
|
);
|
|
|
|
|
expect(find.textContaining('test.example.org'), findsWidgets);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('offers no SimpleLogin where the instance has none', (
|
|
|
|
|
tester,
|
|
|
|
|
) async {
|
|
|
|
|
dependencies.simpleLoginGateway.isOffered = false;
|
|
|
|
|
await start(tester);
|
|
|
|
|
|
|
|
|
|
expect(find.text('Continue with SimpleLogin'), findsNothing);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('someone new creates an account: the handle suggested, the '
|
|
|
|
|
'password twice, the phrase written down, then signed in', (
|
|
|
|
|
tester,
|
|
|
|
|
) async {
|
|
|
|
|
await start(tester);
|
|
|
|
|
|
|
|
|
|
await tapButton(tester, 'Continue with SimpleLogin');
|
|
|
|
|
expect(find.text('New here?'), findsOneWidget);
|
|
|
|
|
await tester.tap(find.widgetWithText(ListTile, 'Create an account'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
expect(find.text('anna_mueller'), findsOneWidget);
|
|
|
|
|
expect(find.text('@anna_mueller is free'), findsOneWidget);
|
|
|
|
|
await tester.enterText(field('Handle'), 'anna_m');
|
|
|
|
|
await tester.pump();
|
|
|
|
|
expect(find.text('@anna_mueller is free'), findsNothing);
|
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(find.text('Checking…'), findsOneWidget);
|
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 tester.enterText(field('Handle'), 'anna_mueller');
|
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
|
|
|
await tester.pump();
|
|
|
|
|
// The suggestion is free without asking (ADR 0020, D30).
|
|
|
|
|
expect(find.text('@anna_mueller is free'), findsOneWidget);
|
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 tapButton(tester, 'Continue');
|
|
|
|
|
|
|
|
|
|
await tester.enterText(field('New password'), password);
|
|
|
|
|
await tester.enterText(field('New password again'), password);
|
|
|
|
|
await tapButton(tester, 'Continue');
|
|
|
|
|
expect(find.text('Your recovery phrase'), findsOneWidget);
|
|
|
|
|
await tapButton(tester, "I've written it down");
|
|
|
|
|
|
|
|
|
|
await typeCheckedWords(tester, fakePhrase);
|
|
|
|
|
await tapButton(tester, 'Create the account');
|
|
|
|
|
|
|
|
|
|
expect(find.text('App of person-1'), findsOneWidget);
|
|
|
|
|
});
|
|
|
|
|
|
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
|
|
|
testWidgets('someone new is told a handle is taken as they type it, and '
|
|
|
|
|
'goes no further with it', (tester) async {
|
|
|
|
|
dependencies.simpleLoginGateway.addAccount(
|
|
|
|
|
'person-9',
|
|
|
|
|
subject: 'simplelogin-other',
|
|
|
|
|
handle: 'anna',
|
|
|
|
|
keys: (await dependencies.crypto.newKeys(password)).setup,
|
|
|
|
|
);
|
|
|
|
|
await start(tester);
|
|
|
|
|
await tapButton(tester, 'Continue with SimpleLogin');
|
|
|
|
|
await tester.tap(find.widgetWithText(ListTile, 'Create an account'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
await tester.enterText(field('Handle'), '@anna');
|
|
|
|
|
await tester.pump(HandleCheckCubit.pause);
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
expect(find.text('Someone else has this handle.'), findsOneWidget);
|
|
|
|
|
await tapButton(tester, 'Continue');
|
|
|
|
|
expect(field('New password'), findsNothing);
|
|
|
|
|
|
|
|
|
|
await tester.enterText(field('Handle'), 'anna_k');
|
|
|
|
|
await tester.pump(HandleCheckCubit.pause);
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
expect(find.text('@anna_k is free'), findsOneWidget);
|
|
|
|
|
await tapButton(tester, 'Continue');
|
|
|
|
|
expect(field('New password'), findsOneWidget);
|
|
|
|
|
});
|
|
|
|
|
|
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
|
|
|
testWidgets('an account opens its keys with its password, a wrong one '
|
|
|
|
|
'refused', (tester) async {
|
|
|
|
|
final material = await dependencies.crypto.newKeys(password);
|
|
|
|
|
dependencies.simpleLoginGateway.addAccount(
|
|
|
|
|
'person-9',
|
|
|
|
|
subject: 'simplelogin-anna',
|
|
|
|
|
handle: 'anna',
|
|
|
|
|
keys: material.setup,
|
|
|
|
|
);
|
|
|
|
|
await start(tester);
|
|
|
|
|
|
|
|
|
|
await tapButton(tester, 'Continue with SimpleLogin');
|
|
|
|
|
expect(find.textContaining('as @anna'), findsOneWidget);
|
|
|
|
|
await tester.enterText(field('Password'), 'wrong horse battery');
|
|
|
|
|
await tapButton(tester, 'Open');
|
|
|
|
|
expect(find.text("The password isn't right."), findsOneWidget);
|
|
|
|
|
|
|
|
|
|
await tester.enterText(field('Password'), password);
|
|
|
|
|
await tapButton(tester, 'Open');
|
|
|
|
|
|
|
|
|
|
expect(find.text('App of person-9'), findsOneWidget);
|
|
|
|
|
});
|
|
|
|
|
|
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
|
|
|
testWidgets('an account whose password was lost opens its keys with the '
|
|
|
|
|
'recovery phrase under a new password, a phrase that is not its own '
|
|
|
|
|
'refused', (tester) async {
|
|
|
|
|
final material = await dependencies.crypto.newKeys(password);
|
|
|
|
|
dependencies.simpleLoginGateway.addAccount(
|
|
|
|
|
'person-9',
|
|
|
|
|
subject: 'simplelogin-anna',
|
|
|
|
|
handle: 'anna',
|
|
|
|
|
keys: material.setup,
|
|
|
|
|
);
|
|
|
|
|
await start(tester);
|
|
|
|
|
|
|
|
|
|
await tapButton(tester, 'Continue with SimpleLogin');
|
|
|
|
|
await tester.tap(find.widgetWithText(TextButton, 'Forgot your password?'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
expect(find.text('A new password'), findsOneWidget);
|
|
|
|
|
await tester.tap(find.widgetWithText(TextButton, 'Back'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
expect(find.text('Your password'), findsOneWidget);
|
|
|
|
|
await tester.tap(find.widgetWithText(TextButton, 'Forgot your password?'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
const newPassword = 'a new horse battery';
|
|
|
|
|
await tester.enterText(field('Recovery phrase'), otherFakePhrase);
|
|
|
|
|
await tester.enterText(field('New password'), newPassword);
|
|
|
|
|
await tester.enterText(field('New password again'), newPassword);
|
|
|
|
|
await tapButton(tester, 'Sign in');
|
|
|
|
|
expect(find.text("The recovery phrase isn't right."), findsOneWidget);
|
|
|
|
|
|
|
|
|
|
await tester.enterText(field('Recovery phrase'), fakePhrase);
|
|
|
|
|
await tapButton(tester, 'Sign in');
|
|
|
|
|
|
|
|
|
|
expect(find.text('App of person-9'), findsOneWidget);
|
|
|
|
|
expect(dependencies.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
|
|
|
testWidgets('someone whose SimpleLogin account was lost links theirs '
|
|
|
|
|
'again with its handle and recovery phrase', (tester) async {
|
|
|
|
|
final material = await dependencies.crypto.newKeys(password);
|
|
|
|
|
dependencies.simpleLoginGateway.addAccount(
|
|
|
|
|
'person-9',
|
|
|
|
|
subject: 'simplelogin-lost',
|
|
|
|
|
handle: 'anna',
|
|
|
|
|
keys: material.setup,
|
|
|
|
|
);
|
|
|
|
|
await start(tester);
|
|
|
|
|
|
|
|
|
|
await tapButton(tester, 'Continue with SimpleLogin');
|
|
|
|
|
await tester.tap(find.widgetWithText(ListTile, 'I already have one'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
expect(find.text('Link your account'), findsOneWidget);
|
|
|
|
|
expect(find.text('anna_mueller'), findsNothing);
|
|
|
|
|
|
|
|
|
|
await tester.enterText(field('Handle'), 'anna');
|
|
|
|
|
await tester.enterText(find.byType(TextField).last, otherFakePhrase);
|
|
|
|
|
await tapButton(tester, 'Link');
|
|
|
|
|
expect(
|
|
|
|
|
find.text("The handle or the recovery phrase isn't right."),
|
|
|
|
|
findsOneWidget,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await tester.enterText(find.byType(TextField).last, fakePhrase);
|
|
|
|
|
await tapButton(tester, 'Link');
|
|
|
|
|
|
|
|
|
|
expect(find.text('App of person-9'), findsOneWidget);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('closing the browser leaves the start screen as it was', (
|
|
|
|
|
tester,
|
|
|
|
|
) async {
|
|
|
|
|
dependencies.browser.comesBack = () => null;
|
|
|
|
|
await start(tester);
|
|
|
|
|
|
|
|
|
|
await tapButton(tester, 'Continue with SimpleLogin');
|
|
|
|
|
|
|
|
|
|
expect(find.text('New here?'), findsNothing);
|
|
|
|
|
expect(
|
|
|
|
|
find.widgetWithText(FilledButton, 'Continue with SimpleLogin'),
|
|
|
|
|
findsOneWidget,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets("a refusal on SimpleLogin's page shows, and it begins again", (
|
|
|
|
|
tester,
|
|
|
|
|
) async {
|
|
|
|
|
dependencies.browser.comesBack = () => const OidcDenied('access_denied');
|
|
|
|
|
await start(tester);
|
|
|
|
|
|
|
|
|
|
await tapButton(tester, 'Continue with SimpleLogin');
|
|
|
|
|
expect(find.textContaining('nothing was shared'), findsOneWidget);
|
|
|
|
|
|
|
|
|
|
dependencies.browser.comesBack = null;
|
|
|
|
|
await tapButton(tester, 'Try again');
|
|
|
|
|
|
|
|
|
|
expect(find.text('New here?'), findsOneWidget);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
for (final locale in HalkyonAppLocalizations.supportedLocales) {
|
|
|
|
|
testWidgets('every refusal has its words in $locale', (tester) async {
|
|
|
|
|
late BuildContext context;
|
|
|
|
|
await tester.pumpWidget(
|
|
|
|
|
MaterialApp(
|
|
|
|
|
locale: locale,
|
|
|
|
|
localizationsDelegates: testLocalizationsDelegates,
|
|
|
|
|
supportedLocales: HalkyonAppLocalizations.supportedLocales,
|
|
|
|
|
home: Builder(
|
|
|
|
|
builder: (built) {
|
|
|
|
|
context = built;
|
|
|
|
|
return const SizedBox();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
for (final problem in SimpleLoginProblem.values) {
|
|
|
|
|
expect(
|
|
|
|
|
simpleLoginFailureText(context, SimpleLoginFailed(problem)),
|
|
|
|
|
isNotEmpty,
|
|
|
|
|
reason: '$problem',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|