SimpleLoginRepository signs in with SimpleLogin in the browser (OidcBrowser: the app's scheme on Android, iOS and macOS, the app's port on the loopback on Linux and Windows, D9), then makes the account's keys and signs someone new up, opens an account's keys with the password, gives an account of before its first keys, or links a new SimpleLogin account in place of a lost one with the recovery phrase (D7), which opens the keys on the device first: a phrase that doesn't fit sends no proof, and no password is asked after. An anonymous person adding SimpleLogin keeps the keys their device holds. Nothing is the device's until `enter`. SimpleLoginGateway calls halkyon_base's `simpleLogin` and `keys` endpoints (ServerpodSimpleLoginGateway), with a fake for tests, as does FakeOidcBrowser. The calls made on clients of their own (signed out, or as a session not yet the device's) move from the password gateway into BaseCalls, which both share. Fixes BaseProtocol, the client of halkyon_base alone, which didn't read halkyon_oidc's classes: every refusal of SimpleLogin's sign-in on a signed-out call would have reached the app as a failure. OidcBrowser keeps its loopback private, so that a fake can implement it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
124 lines
3.8 KiB
Dart
124 lines
3.8 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:halkyon_app/halkyon_app.dart';
|
|
import 'package:halkyon_base_client/halkyon_base_client.dart';
|
|
import 'package:halkyon_oidc_flutter/halkyon_oidc_flutter.dart'
|
|
show OidcRefusal, OidcRefusedException;
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:http/testing.dart';
|
|
|
|
/// [refusal] as an instance sends it: named by its module.
|
|
String sent(SerializableException refusal) => jsonEncode({
|
|
'className': switch (refusal) {
|
|
EmailSignInRefusedException() => 'halkyon_base.EmailSignInRefusedException',
|
|
GitHubSignInRefusedException() =>
|
|
'halkyon_base.GitHubSignInRefusedException',
|
|
PasswordRefusedException() => 'halkyon_base.PasswordRefusedException',
|
|
OidcRefusedException() => 'halkyon_oidc.OidcRefusedException',
|
|
_ => throw ArgumentError.value(refusal),
|
|
},
|
|
'data': (refusal as SerializableModel).toJson(),
|
|
});
|
|
|
|
void main() {
|
|
test("reads the refusals an instance sends under halkyon_base's name, as "
|
|
"an app's own generated protocol does", () {
|
|
final protocol = BaseProtocol();
|
|
|
|
expect(
|
|
protocol.decodeWithType(
|
|
sent(
|
|
EmailSignInRefusedException(reason: EmailSignInRefusal.wrongCode),
|
|
),
|
|
),
|
|
isA<EmailSignInRefusedException>().having(
|
|
(refusal) => refusal.reason,
|
|
'reason',
|
|
EmailSignInRefusal.wrongCode,
|
|
),
|
|
);
|
|
expect(
|
|
protocol.decodeWithType(
|
|
sent(
|
|
GitHubSignInRefusedException(
|
|
reason: GitHubSignInRefusal.unknownAccount,
|
|
),
|
|
),
|
|
),
|
|
isA<GitHubSignInRefusedException>().having(
|
|
(refusal) => refusal.reason,
|
|
'reason',
|
|
GitHubSignInRefusal.unknownAccount,
|
|
),
|
|
);
|
|
});
|
|
|
|
test("reads OpenID Connect's refusals under its own module's name, which "
|
|
"halkyon_base's SimpleLogin passes on", () {
|
|
expect(
|
|
BaseProtocol().decodeWithType(
|
|
sent(OidcRefusedException(reason: OidcRefusal.unknownAttempt)),
|
|
),
|
|
isA<OidcRefusedException>().having(
|
|
(refusal) => refusal.reason,
|
|
'reason',
|
|
OidcRefusal.unknownAttempt,
|
|
),
|
|
);
|
|
});
|
|
|
|
test("calls halkyon_base's endpoints at the instance, and passes its "
|
|
'refusals on as themselves', () async {
|
|
final asked = <String>[];
|
|
final client = BaseClient(
|
|
'https://learn.example.org/',
|
|
httpClientOverride: MockClient((request) async {
|
|
asked.add(request.url.toString());
|
|
return switch (request.url.path) {
|
|
'/halkyon_base.instance/basics' => http.Response(
|
|
jsonEncode(
|
|
InstanceBasics(
|
|
app: 'learn',
|
|
emailSignIn: true,
|
|
sourceCodeUrl: 'https://code.example.org/learn',
|
|
minimumAppBuild: 21,
|
|
dataLocation: 'EU',
|
|
).toJson(),
|
|
),
|
|
200,
|
|
),
|
|
'/halkyon_base.passwordSignIn/begin' => http.Response(
|
|
sent(
|
|
PasswordRefusedException(
|
|
reason: PasswordRefusal.tooManyAttempts,
|
|
),
|
|
),
|
|
400,
|
|
),
|
|
_ => http.Response('', 404),
|
|
};
|
|
}),
|
|
);
|
|
addTearDown(client.close);
|
|
|
|
final basics = await client.modules.instance.basics();
|
|
expect(basics.emailSignIn, isTrue);
|
|
expect(basics.minimumAppBuild, 21);
|
|
expect(basics.dataLocation, 'EU');
|
|
await expectLater(
|
|
client.modules.passwordSignIn.begin(address: 'anna@example.org'),
|
|
throwsA(
|
|
isA<PasswordRefusedException>().having(
|
|
(refusal) => refusal.reason,
|
|
'reason',
|
|
PasswordRefusal.tooManyAttempts,
|
|
),
|
|
),
|
|
);
|
|
expect(asked, [
|
|
'https://learn.example.org/halkyon_base.instance/basics',
|
|
'https://learn.example.org/halkyon_base.passwordSignIn/begin',
|
|
]);
|
|
});
|
|
}
|