halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
import 'package:flag/flag.dart';
|
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
|
import 'package:halkyon_app/halkyon_app.dart';
|
|
|
|
|
import 'package:halkyon_app/testing.dart';
|
|
|
|
|
import 'package:material_ui/material_ui.dart';
|
|
|
|
|
|
|
|
|
|
/// A signed-in person's app as a test makes one: who it is for, and the
|
|
|
|
|
/// way to Switch account.
|
|
|
|
|
class const _PersonApp(final SignedIn person) extends StatelessWidget {
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) => MaterialApp(
|
|
|
|
|
localizationsDelegates: testLocalizationsDelegates,
|
|
|
|
|
supportedLocales: HalkyonAppLocalizations.supportedLocales,
|
|
|
|
|
home: Scaffold(
|
|
|
|
|
body: Builder(
|
|
|
|
|
builder: (context) => Column(
|
|
|
|
|
children: [
|
|
|
|
|
Text('Home of ${person.authUserId}'),
|
|
|
|
|
TextButton(
|
|
|
|
|
onPressed: () => showAccountSwitcher(context),
|
|
|
|
|
child: const Text('Switch account'),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
final builtIn = Instance.parse('learn.halkyon.eu')!;
|
|
|
|
|
final other = Instance.parse('learn.example.org')!;
|
|
|
|
|
late MemoryInstanceStore store;
|
|
|
|
|
late List<Instance> opened;
|
|
|
|
|
late List<TestAppDependencies> dependencies;
|
|
|
|
|
late List<Instance> forgotten;
|
|
|
|
|
|
|
|
|
|
setUp(() {
|
|
|
|
|
store = MemoryInstanceStore();
|
|
|
|
|
opened = [];
|
|
|
|
|
dependencies = [];
|
|
|
|
|
forgotten = [];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/// Starts the app on a device that [usedBefore] the choice existed, with
|
|
|
|
|
/// [answering] instances answering a check, saying where they keep data
|
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
|
|
|
/// as [locations] has it, and nobody signed in on each where [signedOut]
|
|
|
|
|
/// (a device starting there).
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
Future<InstanceChoice> start(
|
|
|
|
|
WidgetTester tester, {
|
|
|
|
|
bool usedBefore = false,
|
|
|
|
|
bool signedOut = false,
|
|
|
|
|
Set<Instance> answering = const {},
|
|
|
|
|
Map<Instance, InstanceLocations> locations = const {},
|
halkyon_app: data locations as precise as a country or a state
An operator's DATA_LOCATION and BACKUP_LOCATION may name a subdivision
(ISO 3166-2, US-CA, DE-BY) besides the EU or a country. The app adds
"(EU)" to a member country and the country to a subdivision ("Germany
(EU)", "California, United States", "Bavaria, Germany (EU)"), in
English and German, and shows the most specific flag there is: a
subdivision's own where the flag package has one, else its country's.
Subdivision names come from Unicode CLDR 48.2 (tool/subdivision_names.py),
bundled with their licence, which the app registers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 18:47:25 +02:00
|
|
|
SubdivisionNames subdivisionNames = const MemorySubdivisionNames({}),
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
}) async {
|
|
|
|
|
tester.view.physicalSize = const Size(412, 900);
|
|
|
|
|
tester.view.devicePixelRatio = 1;
|
|
|
|
|
addTearDown(tester.view.reset);
|
|
|
|
|
final choice = InstanceChoice(
|
|
|
|
|
store,
|
|
|
|
|
builtIn: builtIn,
|
|
|
|
|
forget: (instance) async => forgotten.add(instance),
|
2026-10-21 18:15:14 +02:00
|
|
|
sharesStorage: (a, b) => false,
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
);
|
|
|
|
|
await choice.load(usedBefore: usedBefore);
|
|
|
|
|
await tester.pumpWidget(
|
|
|
|
|
InstanceGate(
|
|
|
|
|
frame: testFrame(),
|
|
|
|
|
choice: choice,
|
|
|
|
|
checker: FakeInstanceChecker(answering, locations),
|
|
|
|
|
countryNames: MemoryCountryNames(const {
|
|
|
|
|
'CH': {'en': 'Switzerland'},
|
halkyon_app: data locations as precise as a country or a state
An operator's DATA_LOCATION and BACKUP_LOCATION may name a subdivision
(ISO 3166-2, US-CA, DE-BY) besides the EU or a country. The app adds
"(EU)" to a member country and the country to a subdivision ("Germany
(EU)", "California, United States", "Bavaria, Germany (EU)"), in
English and German, and shows the most specific flag there is: a
subdivision's own where the flag package has one, else its country's.
Subdivision names come from Unicode CLDR 48.2 (tool/subdivision_names.py),
bundled with their licence, which the app registers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 18:47:25 +02:00
|
|
|
'DE': {'en': 'Germany'},
|
|
|
|
|
'US': {'en': 'United States'},
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
}),
|
halkyon_app: data locations as precise as a country or a state
An operator's DATA_LOCATION and BACKUP_LOCATION may name a subdivision
(ISO 3166-2, US-CA, DE-BY) besides the EU or a country. The app adds
"(EU)" to a member country and the country to a subdivision ("Germany
(EU)", "California, United States", "Bavaria, Germany (EU)"), in
English and German, and shows the most specific flag there is: a
subdivision's own where the flag package has one, else its country's.
Subdivision names come from Unicode CLDR 48.2 (tool/subdivision_names.py),
bundled with their licence, which the app registers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 18:47:25 +02:00
|
|
|
subdivisionNames: subdivisionNames,
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
readLook: () async => (themeMode: ThemeMode.light, locale: null),
|
|
|
|
|
open: (instance) async {
|
|
|
|
|
opened.add(instance);
|
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
|
|
|
final opening = TestAppDependencies(
|
|
|
|
|
serverName: instance.name,
|
|
|
|
|
gateway: FakeAuthGateway(
|
|
|
|
|
status: signedOut ? const SignedOut() : FakeAuthGateway.person,
|
|
|
|
|
),
|
|
|
|
|
);
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
dependencies.add(opening);
|
|
|
|
|
return opening;
|
|
|
|
|
},
|
|
|
|
|
app: SessionGate(
|
|
|
|
|
look: (context) => (themeMode: ThemeMode.light, locale: null),
|
|
|
|
|
app: _PersonApp.new,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
return choice;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> openSwitcher(WidgetTester tester) async {
|
|
|
|
|
await tester.tap(find.text('Switch account'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
testWidgets('a new install asks for the instance first, opens the '
|
|
|
|
|
"build's in one tap, and asks how to start there", (tester) async {
|
|
|
|
|
await start(tester, signedOut: true);
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
|
|
|
|
|
// The wordmark says it to screen readers, naming the app.
|
|
|
|
|
expect(find.bySemanticsLabel('Welcome to Halkyon Test'), findsOneWidget);
|
|
|
|
|
expect(
|
|
|
|
|
find.text('Choose the instance to keep your notes on.'),
|
|
|
|
|
findsOneWidget,
|
|
|
|
|
reason: "the app's own words",
|
|
|
|
|
);
|
|
|
|
|
expect(opened, isEmpty, reason: 'nothing made anywhere yet');
|
|
|
|
|
expect(find.byType(TextField), findsNothing, reason: 'no address asked');
|
|
|
|
|
|
|
|
|
|
await tester.tap(find.text('learn.halkyon.eu'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
expect(opened, [builtIn]);
|
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
|
|
|
expect(find.text('Start on learn.halkyon.eu:'), findsOneWidget);
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets("the build's instance shows where it keeps people's data, "
|
|
|
|
|
'once it says', (tester) async {
|
|
|
|
|
await start(
|
|
|
|
|
tester,
|
|
|
|
|
answering: {builtIn},
|
|
|
|
|
locations: {
|
|
|
|
|
builtIn: const InstanceLocations(data: 'EU', backups: 'CH'),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(find.text('Data: European Union\nBackups: Switzerland'), findsOne);
|
|
|
|
|
expect(find.byType(Flag), findsOne);
|
|
|
|
|
});
|
|
|
|
|
|
halkyon_app: data locations as precise as a country or a state
An operator's DATA_LOCATION and BACKUP_LOCATION may name a subdivision
(ISO 3166-2, US-CA, DE-BY) besides the EU or a country. The app adds
"(EU)" to a member country and the country to a subdivision ("Germany
(EU)", "California, United States", "Bavaria, Germany (EU)"), in
English and German, and shows the most specific flag there is: a
subdivision's own where the flag package has one, else its country's.
Subdivision names come from Unicode CLDR 48.2 (tool/subdivision_names.py),
bundled with their licence, which the app registers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 18:47:25 +02:00
|
|
|
testWidgets('an instance may say where more precisely: a state, a region', (
|
|
|
|
|
tester,
|
|
|
|
|
) async {
|
|
|
|
|
await start(
|
|
|
|
|
tester,
|
|
|
|
|
answering: {builtIn},
|
|
|
|
|
locations: {
|
|
|
|
|
builtIn: const InstanceLocations(data: 'US-CA', backups: 'DE-BY'),
|
|
|
|
|
},
|
|
|
|
|
subdivisionNames: const MemorySubdivisionNames({
|
|
|
|
|
'DE-BY': {'en': 'Bavaria'},
|
|
|
|
|
'US-CA': {'en': 'California'},
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
find.text(
|
|
|
|
|
'Data: California, United States\nBackups: Bavaria, Germany (EU)',
|
|
|
|
|
),
|
|
|
|
|
findsOne,
|
|
|
|
|
);
|
|
|
|
|
expect(find.byType(Flag), findsOne);
|
|
|
|
|
});
|
|
|
|
|
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
testWidgets('an instance saying nothing, or not answering, comes with the '
|
|
|
|
|
'app, without a flag', (tester) async {
|
|
|
|
|
await start(tester);
|
|
|
|
|
|
|
|
|
|
expect(find.text('Comes with the app'), findsOne);
|
|
|
|
|
expect(find.byType(Flag), findsNothing);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('another instance is checked before the app moves there', (
|
|
|
|
|
tester,
|
|
|
|
|
) async {
|
|
|
|
|
await start(tester, answering: {other});
|
|
|
|
|
|
|
|
|
|
await tester.tap(find.text('Another instance'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
expect(
|
|
|
|
|
FocusManager.instance.primaryFocus?.context
|
|
|
|
|
?.findAncestorWidgetOfExactType<TextField>(),
|
|
|
|
|
isNotNull,
|
|
|
|
|
reason: 'the address is typed at once',
|
|
|
|
|
);
|
|
|
|
|
expect(
|
|
|
|
|
find.widgetWithText(TextField, 'test.example.org'),
|
|
|
|
|
findsOneWidget,
|
|
|
|
|
reason: "the app's example address as the hint",
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await tester.enterText(find.byType(TextField), 'nowhere.example.org');
|
|
|
|
|
await tester.tap(find.text('Continue'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
expect(
|
|
|
|
|
find.text('No instance of Halkyon Test answers at nowhere.example.org.'),
|
|
|
|
|
findsOneWidget,
|
|
|
|
|
);
|
|
|
|
|
expect(opened, isEmpty);
|
|
|
|
|
|
|
|
|
|
await tester.enterText(find.byType(TextField), 'learn.example.org');
|
|
|
|
|
await tester.tap(find.text('Continue'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
expect(opened, [other]);
|
|
|
|
|
});
|
|
|
|
|
|
fix(app)!: instances over plain HTTP are refused, but on this machine
What the app sends an instance (the proofs that sign in, the keys'
wrappings, every request a session carries) must not be read or changed on
the way, so an instance is reached over HTTPS, or over plain HTTP only on
this machine (localhost, 127.0.0.0/8, ::1), as Jellyfin servers are. An
address over plain HTTP elsewhere is refused before anything is sent there.
One kept from before is never opened: the app says why, and offers another
instance or removing it from the device, never dropping it silently.
Development builds still use their own instance over HTTP (an emulator
reaches the developer's machine as 10.0.2.2); releases never.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-21 14:35:21 +02:00
|
|
|
testWidgets('an instance over plain HTTP is refused before anything is '
|
|
|
|
|
'sent there', (tester) async {
|
|
|
|
|
await start(tester, answering: {other});
|
|
|
|
|
await tester.tap(find.text('Another instance'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
await tester.enterText(find.byType(TextField), 'http://learn.example.org');
|
|
|
|
|
await tester.tap(find.text('Continue'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
find.textContaining('reaches instances over HTTPS only'),
|
|
|
|
|
findsOneWidget,
|
|
|
|
|
);
|
|
|
|
|
expect(opened, isEmpty);
|
|
|
|
|
});
|
|
|
|
|
|
2026-11-09 23:15:27 +01:00
|
|
|
testWidgets('a single word is no address, said at once rather than looked '
|
|
|
|
|
"for (the developer's review, 9 Nov 2026)", (tester) async {
|
|
|
|
|
await start(tester, answering: {other});
|
|
|
|
|
await tester.tap(find.text('Another instance'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
await tester.enterText(find.byType(TextField), 'todo');
|
|
|
|
|
await tester.tap(find.text('Continue'));
|
|
|
|
|
await tester.pump();
|
|
|
|
|
|
|
|
|
|
expect(find.text("This isn't an address."), findsOneWidget);
|
|
|
|
|
expect(find.byType(LinearProgressIndicator), findsNothing);
|
|
|
|
|
expect(opened, isEmpty);
|
|
|
|
|
});
|
|
|
|
|
|
fix(app)!: instances over plain HTTP are refused, but on this machine
What the app sends an instance (the proofs that sign in, the keys'
wrappings, every request a session carries) must not be read or changed on
the way, so an instance is reached over HTTPS, or over plain HTTP only on
this machine (localhost, 127.0.0.0/8, ::1), as Jellyfin servers are. An
address over plain HTTP elsewhere is refused before anything is sent there.
One kept from before is never opened: the app says why, and offers another
instance or removing it from the device, never dropping it silently.
Development builds still use their own instance over HTTP (an emulator
reaches the developer's machine as 10.0.2.2); releases never.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-21 14:35:21 +02:00
|
|
|
testWidgets('an instance kept from before over plain HTTP is never opened: '
|
|
|
|
|
'another is used, or it is removed', (tester) async {
|
|
|
|
|
final insecure = Instance.parse('http://learn.example.net')!;
|
|
|
|
|
store = MemoryInstanceStore((
|
|
|
|
|
current: insecure,
|
|
|
|
|
known: [builtIn, insecure],
|
|
|
|
|
));
|
|
|
|
|
await start(tester, answering: {builtIn});
|
|
|
|
|
|
|
|
|
|
expect(opened, isEmpty, reason: 'nothing sent over plain HTTP');
|
|
|
|
|
expect(find.textContaining('learn.example.net is reached over'), findsOne);
|
|
|
|
|
|
|
|
|
|
await tester.tap(find.text('Use another instance'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
expect(opened, [builtIn]);
|
|
|
|
|
expect(store.kept?.known, [builtIn, insecure], reason: 'kept, unused');
|
|
|
|
|
expect(forgotten, isEmpty);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('an instance kept from before over plain HTTP can be removed '
|
|
|
|
|
'from the device', (tester) async {
|
|
|
|
|
final insecure = Instance.parse('http://learn.example.net')!;
|
|
|
|
|
store = MemoryInstanceStore((current: insecure, known: [insecure]));
|
|
|
|
|
await start(tester, answering: {builtIn});
|
|
|
|
|
|
|
|
|
|
await tester.tap(find.text('Remove from this device'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
await tester.tap(find.widgetWithText(FilledButton, 'Remove'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
expect(forgotten, [insecure]);
|
|
|
|
|
expect(opened, [builtIn]);
|
|
|
|
|
expect(store.kept?.known, [builtIn]);
|
|
|
|
|
});
|
|
|
|
|
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
testWidgets('an install from before goes on without asking', (tester) async {
|
|
|
|
|
await start(tester, usedBefore: true);
|
|
|
|
|
|
|
|
|
|
expect(opened, [builtIn]);
|
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
|
|
|
expect(find.text('Home of person-1'), findsOneWidget);
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('Switch account moves between the accounts on the device, '
|
|
|
|
|
'closing the app on the one before, and removes one not in '
|
|
|
|
|
'use', (tester) async {
|
|
|
|
|
store = MemoryInstanceStore((current: builtIn, known: [builtIn, other]));
|
|
|
|
|
await start(
|
|
|
|
|
tester,
|
|
|
|
|
answering: {builtIn, other},
|
|
|
|
|
locations: {
|
|
|
|
|
other: const InstanceLocations(
|
|
|
|
|
data: 'CH',
|
|
|
|
|
backups: InstanceLocations.noBackups,
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
await openSwitcher(tester);
|
|
|
|
|
expect(find.text('learn.halkyon.eu'), findsWidgets);
|
|
|
|
|
// Each account with where its instance says it keeps people's data.
|
|
|
|
|
expect(find.text('Data: Switzerland\nNo backups'), findsOne);
|
|
|
|
|
expect(find.byType(Flag), findsOne);
|
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
|
|
|
expect(find.text('Signed in'), findsOne, reason: 'the account in use');
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
await tester.tap(find.text('learn.example.org'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
expect(opened, [builtIn, other]);
|
|
|
|
|
expect(dependencies.first.disposed, isTrue);
|
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
|
|
|
expect(find.text('Home of person-1'), findsOneWidget);
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
|
|
|
|
|
await openSwitcher(tester);
|
|
|
|
|
await tester.tap(find.byTooltip('Remove account'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
await tester.tap(find.widgetWithText(FilledButton, 'Remove'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
expect(forgotten, [builtIn]);
|
|
|
|
|
expect(store.kept?.known, [other]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('signed out, the start screen names the instance and offers '
|
|
|
|
|
'another, through Switch account', (tester) async {
|
|
|
|
|
store = MemoryInstanceStore((current: builtIn, known: [builtIn]));
|
|
|
|
|
await start(tester, answering: {builtIn, other}, signedOut: true);
|
|
|
|
|
|
|
|
|
|
expect(find.text('Start on learn.halkyon.eu:'), findsOneWidget);
|
|
|
|
|
await tester.tap(find.text('Use another instance'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
await tester.tap(find.text('Add account'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
expect(
|
|
|
|
|
find.text('Choose the instance for the account to add.'),
|
|
|
|
|
findsOneWidget,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await tester.enterText(find.byType(TextField), 'learn.example.org');
|
|
|
|
|
await tester.tap(find.text('Continue'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
expect(opened, [builtIn, other]);
|
|
|
|
|
expect(find.text('Start on learn.example.org:'), findsOneWidget);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets("an instance whose app doesn't open says so, and tries "
|
|
|
|
|
'again', (tester) async {
|
|
|
|
|
store = MemoryInstanceStore((current: builtIn, known: [builtIn]));
|
|
|
|
|
final choice = InstanceChoice(
|
|
|
|
|
store,
|
|
|
|
|
builtIn: builtIn,
|
|
|
|
|
forget: (instance) async {},
|
2026-10-21 18:15:14 +02:00
|
|
|
sharesStorage: (a, b) => false,
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
);
|
|
|
|
|
await choice.load(usedBefore: false);
|
|
|
|
|
var attempts = 0;
|
|
|
|
|
await tester.pumpWidget(
|
|
|
|
|
InstanceGate(
|
|
|
|
|
frame: testFrame(),
|
|
|
|
|
choice: choice,
|
|
|
|
|
checker: FakeInstanceChecker(),
|
|
|
|
|
countryNames: MemoryCountryNames(const {}),
|
|
|
|
|
readLook: () async => (themeMode: ThemeMode.light, locale: null),
|
|
|
|
|
open: (instance) async {
|
|
|
|
|
if (++attempts == 1) {
|
|
|
|
|
throw StateError('disk unavailable');
|
|
|
|
|
}
|
|
|
|
|
return TestAppDependencies();
|
|
|
|
|
},
|
|
|
|
|
app: const Directionality(
|
|
|
|
|
textDirection: TextDirection.ltr,
|
|
|
|
|
child: Text('the app'),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
expect(find.text("Couldn't open the app on learn.halkyon.eu."), findsOne);
|
|
|
|
|
|
|
|
|
|
await tester.tap(find.text('Try again'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
expect(find.text('the app'), findsOne);
|
|
|
|
|
});
|
|
|
|
|
}
|