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>
This commit is contained in:
parent
87671208df
commit
630965d920
1 changed files with 2305 additions and 395 deletions
|
|
@ -0,0 +1,138 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fake_async/fake_async.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
|
||||
void main() {
|
||||
/// The handles asked of the instance, in order.
|
||||
late List<String> asked;
|
||||
|
||||
/// The instance's answers: 'admin' reserved, 'broken' unanswered,
|
||||
/// everything else free.
|
||||
Future<HandleProblem?> check(String handle) async {
|
||||
asked.add(handle);
|
||||
return switch (handle) {
|
||||
'admin' => HandleProblem.reserved,
|
||||
'broken' => throw StateError('offline'),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
setUp(() => asked = []);
|
||||
|
||||
test('asks once typing pauses, only for the handle typed last', () {
|
||||
fakeAsync((async) {
|
||||
final cubit = HandleCheckCubit(check)
|
||||
..typed('b')
|
||||
..typed('be')
|
||||
..typed('ben');
|
||||
expect(
|
||||
cubit.state,
|
||||
const HandleCheckState(handle: 'ben', check: HandleCheck.checking),
|
||||
);
|
||||
async.elapse(HandleCheckCubit.pause - const Duration(milliseconds: 1));
|
||||
expect(asked, isEmpty);
|
||||
|
||||
async.elapse(const Duration(milliseconds: 1));
|
||||
expect(asked, ['ben']);
|
||||
expect(
|
||||
cubit.state,
|
||||
const HandleCheckState(handle: 'ben', check: HandleCheck.free),
|
||||
);
|
||||
unawaited(cubit.close());
|
||||
});
|
||||
});
|
||||
|
||||
test('says why a handle is refused, for that handle only', () {
|
||||
fakeAsync((async) {
|
||||
final cubit = HandleCheckCubit(check)..typed(' @admin ');
|
||||
async.elapse(HandleCheckCubit.pause);
|
||||
|
||||
expect(
|
||||
cubit.state,
|
||||
const HandleCheckState(
|
||||
handle: 'admin',
|
||||
check: HandleCheck.refused,
|
||||
problem: HandleProblem.reserved,
|
||||
),
|
||||
);
|
||||
expect(cubit.state.refuses('@admin'), isTrue);
|
||||
expect(cubit.state.refuses('admin_2'), isFalse);
|
||||
unawaited(cubit.close());
|
||||
});
|
||||
});
|
||||
|
||||
test("drops an answer for a handle that isn't typed any more", () {
|
||||
fakeAsync((async) {
|
||||
final slow = Completer<HandleProblem?>();
|
||||
final cubit = HandleCheckCubit((handle) {
|
||||
asked.add(handle);
|
||||
return handle == 'anna' ? slow.future : Future.value();
|
||||
})..typed('anna');
|
||||
async.elapse(HandleCheckCubit.pause);
|
||||
cubit.typed('anna_m');
|
||||
slow.complete(HandleProblem.taken);
|
||||
async.flushMicrotasks();
|
||||
|
||||
expect(
|
||||
cubit.state,
|
||||
const HandleCheckState(handle: 'anna_m', check: HandleCheck.checking),
|
||||
);
|
||||
async.elapse(HandleCheckCubit.pause);
|
||||
expect(asked, ['anna', 'anna_m']);
|
||||
expect(cubit.state.check, HandleCheck.free);
|
||||
unawaited(cubit.close());
|
||||
});
|
||||
});
|
||||
|
||||
test('asks nothing for nothing typed, nor again for the same handle', () {
|
||||
fakeAsync((async) {
|
||||
final cubit = HandleCheckCubit(check)..typed(' @ ');
|
||||
expect(cubit.state, const HandleCheckState());
|
||||
cubit.typed('ben');
|
||||
async.elapse(HandleCheckCubit.pause);
|
||||
cubit.typed('@ben ');
|
||||
async.elapse(HandleCheckCubit.pause);
|
||||
|
||||
expect(asked, ['ben']);
|
||||
expect(cubit.state.check, HandleCheck.free);
|
||||
unawaited(cubit.close());
|
||||
});
|
||||
});
|
||||
|
||||
test("takes the instance's suggestion as free without asking", () {
|
||||
fakeAsync((async) {
|
||||
final cubit = HandleCheckCubit(check, free: 'anna_m')..typed('anna_m');
|
||||
expect(
|
||||
cubit.state,
|
||||
const HandleCheckState(handle: 'anna_m', check: HandleCheck.free),
|
||||
);
|
||||
async.elapse(HandleCheckCubit.pause);
|
||||
expect(asked, isEmpty);
|
||||
unawaited(cubit.close());
|
||||
});
|
||||
});
|
||||
|
||||
test("says nothing it doesn't know where the instance can't be asked", () {
|
||||
fakeAsync((async) {
|
||||
final cubit = HandleCheckCubit(check)..typed('broken');
|
||||
async.elapse(HandleCheckCubit.pause);
|
||||
|
||||
expect(
|
||||
cubit.state,
|
||||
const HandleCheckState(handle: 'broken', check: HandleCheck.unknown),
|
||||
);
|
||||
expect(cubit.state.refuses('broken'), isFalse);
|
||||
unawaited(cubit.close());
|
||||
});
|
||||
});
|
||||
|
||||
test('asks nothing once closed', () {
|
||||
fakeAsync((async) {
|
||||
unawaited((HandleCheckCubit(check)..typed('ben')).close());
|
||||
async.elapse(HandleCheckCubit.pause);
|
||||
expect(asked, isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue