An account signed in or up to on an instance whose operator approves new accounts, or one suspended, gets no session but a wait: the device keeps its pass, words and handle in the keyring per instance (SecureAccountWaitStore, InstanceStorage.waitOf), the auth status is Waiting, and SessionGate shows WaitScreen in its own app, so the start screen's sheets go with it. Every sign-in flow hands over to it: the password's sign-in, sign-up, reset and account reset, SimpleLogin's sign-in, sign-up and mending; their cubits show no problem then. The wait screen shows the handle, the four words the operator reads out for the person to match, and that Halkyon never asks anyone to read them out (D33); it asks where the wait stands as it opens, every 30 seconds, as the app comes back and when asked. A new account can be deleted with its pass, a suspended one can't (ADR 0017). Approved, it leads to the usual sign-in, once more as before (D31); a pass the instance knows no more ends the wait. The start screen says where new accounts wait for approval, and offers no Create an account where the operator takes none; both refusals (closed, too many waiting) have their words, en and de. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
232 lines
7.5 KiB
Dart
232 lines
7.5 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:halkyon_app/halkyon_app.dart';
|
|
import 'package:halkyon_app/testing.dart';
|
|
|
|
void main() {
|
|
final builtIn = Instance.parse('learn.halkyon.eu')!;
|
|
final other = Instance.parse('learn.example.org')!;
|
|
late MemoryInstanceStore store;
|
|
late List<Instance> forgotten;
|
|
|
|
InstanceChoice choice() => InstanceChoice(
|
|
store,
|
|
builtIn: builtIn,
|
|
forget: (instance) async {
|
|
forgotten.add(instance);
|
|
},
|
|
sharesStorage: InstanceStorage('/support', builtIn: builtIn).sharesWith,
|
|
);
|
|
|
|
setUp(() {
|
|
store = MemoryInstanceStore();
|
|
forgotten = [];
|
|
});
|
|
|
|
test('a new install has no instance until one is chosen', () async {
|
|
final instances = choice();
|
|
await instances.load(usedBefore: false);
|
|
|
|
expect(instances.current, isNull);
|
|
expect(instances.known, isEmpty);
|
|
|
|
await instances.use(other);
|
|
expect(instances.current, other);
|
|
expect(store.kept?.current, other);
|
|
expect(store.kept?.known, [other]);
|
|
});
|
|
|
|
test('an install from before there was a choice goes on with the '
|
|
"build's instance", () async {
|
|
final instances = choice();
|
|
await instances.load(usedBefore: true);
|
|
|
|
expect(instances.current, builtIn);
|
|
expect(store.kept?.current, builtIn);
|
|
expect(store.kept?.known, [builtIn]);
|
|
});
|
|
|
|
test('what was chosen is read back', () async {
|
|
store = MemoryInstanceStore((current: other, known: [builtIn, other]));
|
|
final instances = choice();
|
|
await instances.load(usedBefore: true);
|
|
|
|
expect(instances.current, other);
|
|
expect(instances.known, [builtIn, other]);
|
|
});
|
|
|
|
test('switching keeps every account; removing one forgets what the '
|
|
'device holds of it, never of the one in use', () async {
|
|
final instances = choice();
|
|
await instances.load(usedBefore: true);
|
|
await instances.use(other);
|
|
await instances.use(builtIn);
|
|
expect(instances.known, [builtIn, other]);
|
|
|
|
await instances.remove(builtIn);
|
|
expect(forgotten, isEmpty, reason: 'in use');
|
|
|
|
await instances.remove(other);
|
|
expect(forgotten, [other]);
|
|
expect(instances.known, [builtIn]);
|
|
expect(store.kept?.current, builtIn);
|
|
expect(store.kept?.known, [builtIn]);
|
|
});
|
|
|
|
test('tells its listeners of every change', () async {
|
|
final instances = choice();
|
|
var changes = 0;
|
|
instances.addListener(() => changes++);
|
|
|
|
await instances.load(usedBefore: false);
|
|
await instances.use(other);
|
|
await instances.use(builtIn);
|
|
await instances.remove(other);
|
|
|
|
expect(changes, 4);
|
|
});
|
|
|
|
test('allows an instance reached securely, and over plain HTTP elsewhere '
|
|
"only the build's own in a development build", () {
|
|
final insecure = Instance.parse('http://10.0.2.2:8080')!;
|
|
// A development build, as tests run.
|
|
final development = InstanceChoice(
|
|
store,
|
|
builtIn: insecure,
|
|
forget: (instance) async {},
|
|
sharesStorage: (a, b) => false,
|
|
);
|
|
expect(development.allows(builtIn), isTrue);
|
|
expect(
|
|
development.allows(Instance.parse('http://localhost:8080')!),
|
|
isTrue,
|
|
);
|
|
expect(development.allows(insecure), isTrue);
|
|
expect(
|
|
development.allows(Instance.parse('http://learn.example.org')!),
|
|
isFalse,
|
|
);
|
|
|
|
final release = InstanceChoice(
|
|
store,
|
|
builtIn: insecure,
|
|
forget: (instance) async {},
|
|
allowsInsecureBuiltIn: false,
|
|
sharesStorage: (a, b) => false,
|
|
);
|
|
expect(release.allows(insecure), isFalse);
|
|
});
|
|
|
|
test('leaves an instance it no longer allows for another, forgetting it '
|
|
'if asked', () async {
|
|
final insecure = Instance.parse('http://learn.example.net')!;
|
|
store = MemoryInstanceStore((
|
|
current: insecure,
|
|
known: [other, insecure, builtIn],
|
|
));
|
|
final instances = choice();
|
|
await instances.load(usedBefore: false);
|
|
expect(instances.current, insecure, reason: 'never dropped silently');
|
|
|
|
await instances.leave(insecure, forget: false);
|
|
expect(instances.current, other, reason: 'the first allowed one');
|
|
expect(instances.known, [other, insecure, builtIn]);
|
|
expect(forgotten, isEmpty);
|
|
|
|
await instances.use(insecure);
|
|
await instances.leave(insecure, forget: true);
|
|
expect(instances.current, other);
|
|
expect(instances.known, [other, builtIn]);
|
|
expect(forgotten, [insecure]);
|
|
expect(store.kept?.known, [other, builtIn]);
|
|
});
|
|
|
|
test("leaving the only one goes to the build's own", () async {
|
|
final insecure = Instance.parse('http://learn.example.net')!;
|
|
store = MemoryInstanceStore((current: insecure, known: [insecure]));
|
|
final instances = choice();
|
|
await instances.load(usedBefore: false);
|
|
|
|
await instances.leave(insecure, forget: true);
|
|
expect(instances.current, builtIn);
|
|
expect(instances.known, [builtIn]);
|
|
expect(forgotten, [insecure]);
|
|
});
|
|
|
|
group('two addresses the device keeps in one place (http:// and https:// '
|
|
'on one host; the reading of the fixes)', () {
|
|
final insecure = Instance.parse('http://learn.example.org')!;
|
|
|
|
test('are one account: choosing one puts it in the place of the other, '
|
|
'forgetting nothing', () async {
|
|
store = MemoryInstanceStore((
|
|
current: insecure,
|
|
known: [builtIn, insecure],
|
|
));
|
|
final instances = choice();
|
|
await instances.load(usedBefore: false);
|
|
|
|
await instances.use(other);
|
|
|
|
expect(instances.current, other);
|
|
expect(instances.known, [builtIn, other]);
|
|
expect(forgotten, isEmpty);
|
|
});
|
|
|
|
test('removing one never forgets the folder, session or keys the other '
|
|
'uses', () async {
|
|
// Both known, from before.
|
|
store = MemoryInstanceStore((
|
|
current: insecure,
|
|
known: [builtIn, insecure, other],
|
|
));
|
|
final instances = choice();
|
|
await instances.load(usedBefore: false);
|
|
|
|
// Refused, and left for good, as the screen for an address over
|
|
// plain HTTP offers.
|
|
await instances.leave(insecure, forget: true);
|
|
expect(instances.known, [builtIn, other]);
|
|
expect(forgotten, isEmpty);
|
|
|
|
store = MemoryInstanceStore((
|
|
current: other,
|
|
known: [builtIn, insecure, other],
|
|
));
|
|
final again = choice();
|
|
await again.load(usedBefore: false);
|
|
await again.remove(insecure);
|
|
expect(again.known, [builtIn, other]);
|
|
expect(forgotten, isEmpty);
|
|
});
|
|
});
|
|
|
|
test('the storage tells which addresses it keeps in one place, the '
|
|
"build's own apart", () {
|
|
final storage = InstanceStorage('/support', builtIn: builtIn);
|
|
final insecure = Instance.parse('http://learn.example.org')!;
|
|
final builtInsTwin = Instance.parse('http://learn.halkyon.eu')!;
|
|
|
|
expect(storage.sharesWith(insecure, other), isTrue);
|
|
expect(storage.sharesWith(other, other), isFalse);
|
|
expect(storage.sharesWith(builtIn, builtInsTwin), isFalse);
|
|
expect(
|
|
storage.sharesWith(other, Instance.parse('learn.example.net')!),
|
|
isFalse,
|
|
);
|
|
// Their places as before: none moved.
|
|
expect(
|
|
storage.peopleOf(other).path,
|
|
'/support/instances/learn.example.org/people',
|
|
);
|
|
expect(storage.peopleOf(builtIn).path, '/support/people');
|
|
});
|
|
|
|
test("an account's wait is kept per instance, as its keys are (Halkyon "
|
|
"Learn's ADR 0021, D31)", () {
|
|
final storage = InstanceStorage('/support', builtIn: builtIn);
|
|
|
|
expect(storage.waitOf(builtIn), 'halkyon_account_wait');
|
|
expect(storage.waitOf(other), 'halkyon_account_wait@learn.example.org');
|
|
expect(storage.waitOf(other), isNot(storage.keysOf(other)));
|
|
});
|
|
}
|