halkyon/packages/halkyon_base_server/test/integration/instance/instance_test.dart

91 lines
2.6 KiB
Dart
Raw Normal View History

import 'package:halkyon_base_server/base.dart';
import 'package:serverpod/serverpod.dart';
import 'package:test/test.dart';
import '../support/fixtures.dart';
import '../test_tools/serverpod_test_tools.dart';
/// Keeps nothing; there to say the instance can mail codes.
final class _NoMail implements CodeMailer {
@override
Future<void> send(
Session session, {
required String to,
required String code,
required String language,
}) async {}
feat(auth): a password with TOTP on the register, and resets The register's side of signing in with an address, a password that never leaves the device (SRP-6a) and a code from an authenticator (ADR 0017), with members forwarding every step through AccountRegister: - password.set: salt, Argon2id's costs, SRP's verifier, the public keys and the private keys wrapped twice, all made on the device; with the address a mailed code proves, an anonymous person becomes identified under their ID. It signs in once an authenticator is confirmed. - passwordSignIn.begin and complete: SRP's two steps with a TOTP or recovery code. An address nobody has is answered from a decoy key as an account would be; every wrong credential is refused alike. - authenticator: secrets sealed under totpKey (XChaCha20-Poly1305, bound to their row), each code accepted once, ten recovery codes with the first, the last one kept. - accountRecovery: a lost authenticator reset 72 hours after the password and a mailed code or the recovery phrase ask, a lost password set anew with the phrase's signature, or the account reset with new keys 72 hours after a mailed code asks; a mail at each start, a sign-in calling it off. - An account with a password no longer signs in by a mailed code or GitHub; no new GitHub account is linked where passwords are offered, and from GITHUB_SIGN_IN_ENDS GitHub signs nobody in and its IDs go. Five tables, added by the module's migration; deletePerson deletes their rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 13:32:12 +02:00
@override
Future<void> sendResetNotice(
Session session, {
required String to,
required AccountResetKind kind,
required DateTime takesEffectAt,
required String language,
}) async {}
}
void main() {
withServerpod('What every instance tells its apps', (
sessionBuilder,
endpoints,
) {
late InstanceBasics basics;
setUp(() async {
useBase(sessionBuilder);
basics = await endpoints.instance.basics(
sessionBuilder.copyWith(
authentication: AuthenticationOverride.unauthenticated(),
),
);
});
test('names the app it serves, so an app recognises its own instances', () {
expect(basics.app, testApp);
});
test("names where the host's project publishes its source when the "
'operator set nothing', () {
// The tests run without SOURCE_URL.
expect(basics.sourceCodeUrl, testUpstreamSourceCodeUrl);
});
test('names the oldest app build the host works with, to anyone, so an '
'older app can say it needs an update', () {
expect(basics.minimumAppBuild, testMinimumAppBuild);
});
test('says nothing of where its data and backups are where the '
'operator said nothing', () {
// The tests run without DATA_LOCATION and BACKUP_LOCATION.
expect(basics.dataLocation, isNull);
expect(basics.backupLocation, isNull);
});
test('offers no GitHub sign-in without an OAuth app of its own', () {
// The tests' passwords hold no githubClientId.
expect(basics.gitHubClientId, isNull);
});
test('offers email sign-in only where it can mail codes', () async {
final codes = EmailCodes.of(sessionBuilder.build().serverpod);
final unauthenticated = sessionBuilder.copyWith(
authentication: AuthenticationOverride.unauthenticated(),
);
addTearDown(() => codes.mailer = null);
codes.mailer = null;
expect(
(await endpoints.instance.basics(unauthenticated)).emailSignIn,
isFalse,
);
codes.mailer = _NoMail();
expect(
(await endpoints.instance.basics(unauthenticated)).emailSignIn,
isTrue,
);
});
});
}