halkyon_base: a Serverpod module for sign-in, accounts and instance basics
What every Halkyon server has, taken from Halkyon Learn's: anonymous, GitHub and email-code sign-in with their endpoints, an account's ways in and its deletion, the pruning of abandoned anonymous people, of sign-up attempts and of expired codes, and the instance's basics (the sign-in ways offered, the source link, the minimum app build, where people's data and its backups are). Endpoints sit under the nickname `base`. A host configures it in one place before its pod starts: its name for the mails, its source link, its minimum build, and hooks that delete its own data with a person. The tables keep Learn's names, columns and indexes, so a host moving onto the module migrates nothing but version rows. Server package under the SSPL, client under the AGPL, as Learn's are. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
80fa7b9628
commit
af1de799f4
9 changed files with 17704 additions and 3 deletions
|
|
@ -0,0 +1,282 @@
|
|||
import 'package:halkyon_base_server/base.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../support/fixtures.dart';
|
||||
import '../test_tools/serverpod_test_tools.dart';
|
||||
|
||||
void main() {
|
||||
withServerpod('Deleting an account', (sessionBuilder, endpoints) {
|
||||
late Session session;
|
||||
late UuidValue anna;
|
||||
late UuidValue annasPhone;
|
||||
late UuidValue ben;
|
||||
|
||||
/// What the host's deletions saw, in the order they ran.
|
||||
late List<String> seen;
|
||||
|
||||
/// The host's deletions: each notes what it finds of the person while it
|
||||
/// runs, and what runs once the deletion holds.
|
||||
List<PersonDataDeletion> noting(String name) => [
|
||||
(session, person, transaction) async {
|
||||
final sessions = await ServerSideSession.db.count(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(person),
|
||||
transaction: transaction,
|
||||
);
|
||||
final stillThere = await AuthUser.db.findById(
|
||||
session,
|
||||
person,
|
||||
transaction: transaction,
|
||||
);
|
||||
seen.add(
|
||||
'$name: $sessions sessions, '
|
||||
'person ${stillThere == null ? 'gone' : 'there'}',
|
||||
);
|
||||
return () async {
|
||||
final gone = await AuthUser.db.findById(session, person) == null;
|
||||
seen.add('after $name: person ${gone ? 'gone' : 'there'}');
|
||||
};
|
||||
},
|
||||
];
|
||||
|
||||
Future<void> deleteAnna() => endpoints.account.delete(
|
||||
sessionBuilder.copyWith(
|
||||
authentication: AuthenticationOverride.authenticationInfo(
|
||||
anna.uuid,
|
||||
{},
|
||||
authId: annasPhone.uuid,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
setUp(() async {
|
||||
seen = [];
|
||||
useBase(
|
||||
sessionBuilder,
|
||||
deletePersonData: [...noting('first'), ...noting('second')],
|
||||
);
|
||||
session = sessionBuilder.build();
|
||||
anna = await newAnonymousPerson(session);
|
||||
annasPhone = await signInNewDevice(session, anna);
|
||||
await signInNewDevice(session, anna);
|
||||
ben = await newAnonymousPerson(session);
|
||||
await signInNewDevice(session, ben);
|
||||
});
|
||||
|
||||
test('refuses callers who are not signed in', () async {
|
||||
await expectLater(
|
||||
endpoints.account.delete(
|
||||
sessionBuilder.copyWith(
|
||||
authentication: AuthenticationOverride.unauthenticated(),
|
||||
),
|
||||
),
|
||||
throwsA(isA<ServerpodUnauthenticatedException>()),
|
||||
);
|
||||
expect(seen, isEmpty);
|
||||
});
|
||||
|
||||
test('deletes the caller with their sessions and ways in, and nobody '
|
||||
'else', () async {
|
||||
await EmailIdentity.db.insert(session, [
|
||||
for (final (person, hash) in [(anna, 'a' * 64), (ben, 'b' * 64)])
|
||||
EmailIdentity(
|
||||
authUserId: person,
|
||||
addressHash: hash,
|
||||
createdAt: DateTime.now().toUtc(),
|
||||
),
|
||||
]);
|
||||
await GitHubAccount.db.insertRow(
|
||||
session,
|
||||
GitHubAccount(userIdentifier: '4711', authUserId: anna),
|
||||
);
|
||||
|
||||
await deleteAnna();
|
||||
|
||||
expect(await AuthUser.db.findById(session, anna), isNull);
|
||||
expect(await _sessionsOf(session, anna), 0);
|
||||
expect(await _anonymousAccountsOf(session, anna), 0);
|
||||
expect(
|
||||
await EmailIdentity.db.count(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(anna),
|
||||
),
|
||||
0,
|
||||
);
|
||||
expect(
|
||||
await GitHubAccount.db.count(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(anna),
|
||||
),
|
||||
0,
|
||||
);
|
||||
|
||||
expect(await AuthUser.db.findById(session, ben), isNotNull);
|
||||
expect(await _sessionsOf(session, ben), 1);
|
||||
expect(await _anonymousAccountsOf(session, ben), 1);
|
||||
expect(
|
||||
await EmailIdentity.db.count(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(ben),
|
||||
),
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
test("runs the host's deletions in order, once the sessions ended and "
|
||||
'before the person goes, and what they leave for afterwards once '
|
||||
'it holds', () async {
|
||||
await deleteAnna();
|
||||
|
||||
expect(seen, [
|
||||
'first: 0 sessions, person there',
|
||||
'second: 0 sessions, person there',
|
||||
'after first: person gone',
|
||||
'after second: person gone',
|
||||
]);
|
||||
});
|
||||
|
||||
test("leaves everything as it was when a host's deletion fails, and "
|
||||
'runs nothing afterwards', () async {
|
||||
useBase(
|
||||
sessionBuilder,
|
||||
deletePersonData: [
|
||||
...noting('first'),
|
||||
(_, _, _) async => throw StateError('the host failed'),
|
||||
],
|
||||
);
|
||||
|
||||
await expectLater(deleteAnna(), throwsA(isA<StateError>()));
|
||||
|
||||
expect(await AuthUser.db.findById(session, anna), isNotNull);
|
||||
expect(await _sessionsOf(session, anna), 2);
|
||||
expect(await _anonymousAccountsOf(session, anna), 1);
|
||||
expect(seen, ['first: 0 sessions, person there']);
|
||||
});
|
||||
});
|
||||
|
||||
withServerpod('Forgetting sign-up attempts', (sessionBuilder, _) {
|
||||
late Session session;
|
||||
final now = DateTime.now().toUtc();
|
||||
|
||||
setUp(() => session = sessionBuilder.build());
|
||||
|
||||
test('deletes those the rate limit is done with, with their IP '
|
||||
'addresses, and keeps the rest', () async {
|
||||
Future<UuidValue> attempt(Duration ago) async =>
|
||||
(await RateLimitedRequestAttempt.db.insertRow(
|
||||
session,
|
||||
RateLimitedRequestAttempt(
|
||||
domain: 'anonymous',
|
||||
source: 'account_creation',
|
||||
key: '203.0.113.7',
|
||||
ipAddress: '203.0.113.7',
|
||||
attemptedAt: now.subtract(ago),
|
||||
),
|
||||
)).id!;
|
||||
final old = await attempt(const Duration(days: 3));
|
||||
final done = await attempt(const Duration(minutes: 61));
|
||||
final recent = await attempt(const Duration(minutes: 59));
|
||||
|
||||
// Others may have been left by earlier runs.
|
||||
expect(
|
||||
await forgetOldSignUpAttempts(session, now: now),
|
||||
greaterThanOrEqualTo(2),
|
||||
);
|
||||
|
||||
for (final forgotten in [old, done]) {
|
||||
expect(
|
||||
await RateLimitedRequestAttempt.db.findById(session, forgotten),
|
||||
isNull,
|
||||
);
|
||||
}
|
||||
expect(
|
||||
await RateLimitedRequestAttempt.db.findById(session, recent),
|
||||
isNotNull,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
withServerpod('Pruning abandoned anonymous people', (sessionBuilder, _) {
|
||||
late Session session;
|
||||
final now = DateTime.now().toUtc();
|
||||
DateTime daysAgo(int days) => now.subtract(Duration(days: days));
|
||||
|
||||
/// Whom the host's deletion was asked to delete.
|
||||
late List<UuidValue> deletedByHost;
|
||||
|
||||
setUp(() {
|
||||
deletedByHost = [];
|
||||
useBase(
|
||||
sessionBuilder,
|
||||
deletePersonData: [
|
||||
(_, person, _) async {
|
||||
deletedByHost.add(person);
|
||||
return null;
|
||||
},
|
||||
],
|
||||
);
|
||||
session = sessionBuilder.build();
|
||||
});
|
||||
|
||||
test(
|
||||
'deletes anonymous people unused for 90 days, and only them, with '
|
||||
"the host's data of theirs",
|
||||
() async {
|
||||
final unused = await newAnonymousPerson(
|
||||
session,
|
||||
createdAt: daysAgo(100),
|
||||
);
|
||||
await _lastUsed(session, unused, daysAgo(95));
|
||||
final neverSignedIn = await newAnonymousPerson(
|
||||
session,
|
||||
createdAt: daysAgo(100),
|
||||
);
|
||||
final recentlyUsed = await newAnonymousPerson(
|
||||
session,
|
||||
createdAt: daysAgo(100),
|
||||
);
|
||||
await _lastUsed(session, recentlyUsed, daysAgo(10));
|
||||
final newcomer = await newAnonymousPerson(
|
||||
session,
|
||||
createdAt: daysAgo(5),
|
||||
);
|
||||
final identifiedPerson = (await AuthServices.instance.authUsers.create(
|
||||
session,
|
||||
scopes: {identified},
|
||||
)).id;
|
||||
await _lastUsed(session, identifiedPerson, daysAgo(365));
|
||||
|
||||
final pruned = await pruneAbandonedAnonymousPeople(session, now: now);
|
||||
|
||||
expect(pruned, 2);
|
||||
expect(await AuthUser.db.findById(session, unused), isNull);
|
||||
expect(await AuthUser.db.findById(session, neverSignedIn), isNull);
|
||||
for (final kept in [recentlyUsed, newcomer, identifiedPerson]) {
|
||||
expect(await AuthUser.db.findById(session, kept), isNotNull);
|
||||
}
|
||||
expect(deletedByHost, unorderedEquals([unused, neverSignedIn]));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Signs [person] in on a device last used at [at].
|
||||
Future<void> _lastUsed(Session session, UuidValue person, DateTime at) async {
|
||||
final device = await signInNewDevice(session, person);
|
||||
final row = await ServerSideSession.db.findById(session, device);
|
||||
await ServerSideSession.db.updateRow(session, row!.copyWith(lastUsedAt: at));
|
||||
}
|
||||
|
||||
Future<int> _sessionsOf(Session session, UuidValue person) =>
|
||||
ServerSideSession.db.count(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(person),
|
||||
);
|
||||
|
||||
Future<int> _anonymousAccountsOf(Session session, UuidValue person) =>
|
||||
AnonymousAccount.db.count(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(person),
|
||||
);
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../support/fixtures.dart';
|
||||
import '../test_tools/serverpod_test_tools.dart';
|
||||
|
||||
void main() {
|
||||
withServerpod(
|
||||
'Anonymous sign-in',
|
||||
// Signing in runs concurrent transactions (the rate limit and the new
|
||||
// account), which the test's database rollback can't wrap. So these tests
|
||||
// don't roll back; they delete what they create instead.
|
||||
rollbackDatabase: RollbackDatabase.disabled,
|
||||
(sessionBuilder, endpoints) {
|
||||
setUp(() async {
|
||||
useBase(sessionBuilder);
|
||||
await _deleteAll(sessionBuilder.build());
|
||||
});
|
||||
tearDown(() => _deleteAll(sessionBuilder.build()));
|
||||
|
||||
test('creates a person', () async {
|
||||
final authSuccess = await endpoints.anonymousIdp.login(sessionBuilder);
|
||||
|
||||
final authUser = await AuthServices.instance.authUsers.get(
|
||||
sessionBuilder.build(),
|
||||
authUserId: authSuccess.authUserId,
|
||||
);
|
||||
expect(authUser.id, authSuccess.authUserId);
|
||||
});
|
||||
|
||||
test(
|
||||
'refuses the 31st account from one IP address within an hour',
|
||||
() async {
|
||||
for (var i = 0; i < 30; i++) {
|
||||
await endpoints.anonymousIdp.login(sessionBuilder);
|
||||
}
|
||||
|
||||
await expectLater(
|
||||
endpoints.anonymousIdp.login(sessionBuilder),
|
||||
throwsA(
|
||||
isA<AnonymousAccountBlockedException>().having(
|
||||
(e) => e.reason,
|
||||
'reason',
|
||||
AnonymousAccountBlockedExceptionReason.tooManyAttempts,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes every rate-limit record and every person; their anonymous accounts
|
||||
/// and sessions go with them (`ON DELETE CASCADE`).
|
||||
Future<void> _deleteAll(Session session) async {
|
||||
await RateLimitedRequestAttempt.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.id.notEquals(null),
|
||||
);
|
||||
await AuthUser.db.deleteWhere(session, where: (t) => t.id.notEquals(null));
|
||||
}
|
||||
|
|
@ -0,0 +1,574 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:halkyon_base_server/base.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../support/fixtures.dart';
|
||||
import '../test_tools/serverpod_test_tools.dart';
|
||||
|
||||
/// Keeps every code mailed, in place of a mail service.
|
||||
final class _Mailbox implements CodeMailer {
|
||||
final sent = <({String to, String code, String language})>[];
|
||||
|
||||
@override
|
||||
Future<void> send(
|
||||
Session session, {
|
||||
required String to,
|
||||
required String code,
|
||||
required String language,
|
||||
}) async {
|
||||
sent.add((to: to, code: code, language: language));
|
||||
}
|
||||
|
||||
/// The last code mailed to [address].
|
||||
String codeFor(String address) =>
|
||||
sent.lastWhere((mail) => mail.to == address).code;
|
||||
}
|
||||
|
||||
/// A mail service that takes no mail.
|
||||
final class _Refusing implements CodeMailer {
|
||||
@override
|
||||
Future<void> send(
|
||||
Session session, {
|
||||
required String to,
|
||||
required String code,
|
||||
required String language,
|
||||
}) async => throw const SocketException('Connection refused');
|
||||
}
|
||||
|
||||
void main() {
|
||||
withServerpod(
|
||||
'Email sign-in',
|
||||
// Mailing and accepting codes count attempts in transactions of their
|
||||
// own (Serverpod's rate limiter), which the test's database rollback
|
||||
// can't wrap. So these tests delete what they make instead.
|
||||
rollbackDatabase: RollbackDatabase.disabled,
|
||||
(sessionBuilder, endpoints) {
|
||||
late Session session;
|
||||
late _Mailbox mailbox;
|
||||
|
||||
/// Anonymous, signed in on her phone.
|
||||
late UuidValue anna;
|
||||
late UuidValue annasPhone;
|
||||
|
||||
/// Identified, signing in with ben@example.org.
|
||||
late UuidValue ben;
|
||||
late UuidValue bensLaptop;
|
||||
|
||||
/// Identified, signing in with GitHub.
|
||||
late UuidValue carla;
|
||||
late UuidValue carlasPhone;
|
||||
|
||||
setUp(() async {
|
||||
useBase(sessionBuilder);
|
||||
session = sessionBuilder.build();
|
||||
await _deleteAll(session);
|
||||
mailbox = _Mailbox();
|
||||
EmailCodes.of(session.serverpod).mailer = mailbox;
|
||||
|
||||
anna = (await AuthServices.instance.authUsers.create(session)).id;
|
||||
await AnonymousAccount.db.insertRow(
|
||||
session,
|
||||
AnonymousAccount(authUserId: anna),
|
||||
);
|
||||
annasPhone = await signInNewDevice(session, anna);
|
||||
ben = (await AuthServices.instance.authUsers.create(
|
||||
session,
|
||||
scopes: {identified},
|
||||
)).id;
|
||||
await EmailIdentity.db.insertRow(
|
||||
session,
|
||||
EmailIdentity(
|
||||
authUserId: ben,
|
||||
addressHash: addressHash(session.serverpod, 'ben@example.org'),
|
||||
createdAt: clock.now(),
|
||||
),
|
||||
);
|
||||
bensLaptop = await signInNewDevice(
|
||||
session,
|
||||
ben,
|
||||
scopes: {identified},
|
||||
);
|
||||
carla = (await AuthServices.instance.authUsers.create(
|
||||
session,
|
||||
scopes: {identified},
|
||||
)).id;
|
||||
await GitHubAccount.db.insertRow(
|
||||
session,
|
||||
GitHubAccount(userIdentifier: '1001', authUserId: carla),
|
||||
);
|
||||
carlasPhone = await signInNewDevice(
|
||||
session,
|
||||
carla,
|
||||
scopes: {identified},
|
||||
);
|
||||
});
|
||||
tearDown(() async {
|
||||
EmailCodes.of(session.serverpod).mailer = null;
|
||||
await _deleteAll(sessionBuilder.build());
|
||||
});
|
||||
|
||||
TestSessionBuilder signedIn(
|
||||
UuidValue person,
|
||||
UuidValue sessionId, {
|
||||
bool isIdentified = false,
|
||||
}) => sessionBuilder.copyWith(
|
||||
authentication: AuthenticationOverride.authenticationInfo(
|
||||
person.uuid,
|
||||
{if (isIdentified) identified},
|
||||
authId: sessionId.uuid,
|
||||
),
|
||||
);
|
||||
TestSessionBuilder asAnna() => signedIn(anna, annasPhone);
|
||||
TestSessionBuilder asBen() =>
|
||||
signedIn(ben, bensLaptop, isIdentified: true);
|
||||
TestSessionBuilder asCarla() =>
|
||||
signedIn(carla, carlasPhone, isIdentified: true);
|
||||
TestSessionBuilder notSignedIn() => sessionBuilder.copyWith(
|
||||
authentication: AuthenticationOverride.unauthenticated(),
|
||||
);
|
||||
|
||||
/// Mails a code to [address] for [as] and types it.
|
||||
Future<EmailCodeAccepted> signInWith(
|
||||
TestSessionBuilder as,
|
||||
String address,
|
||||
) async {
|
||||
final request = await endpoints.emailCode.sendCode(
|
||||
as,
|
||||
address: address,
|
||||
language: 'en',
|
||||
);
|
||||
return await endpoints.emailCode.verifyCode(
|
||||
as,
|
||||
request: request,
|
||||
code: mailbox.codeFor(address.trim().toLowerCase()),
|
||||
);
|
||||
}
|
||||
|
||||
group('refuses', () {
|
||||
test('mailing and accepting codes when not signed in', () async {
|
||||
await expectLater(
|
||||
endpoints.emailCode.sendCode(
|
||||
notSignedIn(),
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
throwsA(isA<ServerpodUnauthenticatedException>()),
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.emailCode.verifyCode(
|
||||
notSignedIn(),
|
||||
request: const Uuid().v7obj(),
|
||||
code: 'K7QX-M2PD',
|
||||
),
|
||||
throwsA(isA<ServerpodUnauthenticatedException>()),
|
||||
);
|
||||
expect(mailbox.sent, isEmpty);
|
||||
});
|
||||
|
||||
test(
|
||||
'everything on an instance that sends no mail, and says so',
|
||||
() async {
|
||||
EmailCodes.of(session.serverpod).mailer = null;
|
||||
|
||||
await expectLater(
|
||||
endpoints.emailCode.sendCode(
|
||||
asAnna(),
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
throwsA(_refusal(EmailSignInRefusal.unavailable)),
|
||||
);
|
||||
expect(
|
||||
(await endpoints.instance.basics(notSignedIn())).emailSignIn,
|
||||
isFalse,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('a code it could not mail, keeping nothing of it', () async {
|
||||
EmailCodes.of(session.serverpod).mailer = _Refusing();
|
||||
|
||||
await expectLater(
|
||||
endpoints.emailCode.sendCode(
|
||||
asAnna(),
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
throwsA(_refusal(EmailSignInRefusal.mailFailed)),
|
||||
);
|
||||
expect(await EmailCodeRequest.db.count(session), 0);
|
||||
expect(await SecretChallenge.db.count(session), 0);
|
||||
});
|
||||
|
||||
test('what is no address', () async {
|
||||
for (final typed in ['anna', 'anna@', '@example.org', 'a b@c.de']) {
|
||||
await expectLater(
|
||||
endpoints.emailCode.sendCode(
|
||||
asAnna(),
|
||||
address: typed,
|
||||
language: 'en',
|
||||
),
|
||||
throwsA(_refusal(EmailSignInRefusal.notAnAddress)),
|
||||
reason: typed,
|
||||
);
|
||||
}
|
||||
expect(mailbox.sent, isEmpty);
|
||||
});
|
||||
|
||||
test('a wrong code, and after three wrong ones even the right one; '
|
||||
'nothing changes', () async {
|
||||
final request = await endpoints.emailCode.sendCode(
|
||||
asAnna(),
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
for (var i = 0; i < 3; i++) {
|
||||
await expectLater(
|
||||
endpoints.emailCode.verifyCode(
|
||||
asAnna(),
|
||||
request: request,
|
||||
code: 'AAAA-AAAA',
|
||||
),
|
||||
throwsA(_refusal(EmailSignInRefusal.wrongCode)),
|
||||
);
|
||||
}
|
||||
await expectLater(
|
||||
endpoints.emailCode.verifyCode(
|
||||
asAnna(),
|
||||
request: request,
|
||||
code: mailbox.codeFor('anna@example.org'),
|
||||
),
|
||||
throwsA(_refusal(EmailSignInRefusal.tooManyTries)),
|
||||
);
|
||||
expect(await _addressesOf(session, anna), isEmpty);
|
||||
});
|
||||
|
||||
test('a code past its 15 minutes, a code used, and one replaced by '
|
||||
'a newer code', () async {
|
||||
final late = await endpoints.emailCode.sendCode(
|
||||
asAnna(),
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
final lateCode = mailbox.codeFor('anna@example.org');
|
||||
await withClock(
|
||||
Clock.fixed(clock.now().add(const Duration(minutes: 16))),
|
||||
() => expectLater(
|
||||
endpoints.emailCode.verifyCode(
|
||||
asAnna(),
|
||||
request: late,
|
||||
code: lateCode,
|
||||
),
|
||||
throwsA(_refusal(EmailSignInRefusal.codeExpired)),
|
||||
),
|
||||
);
|
||||
|
||||
final replaced = await endpoints.emailCode.sendCode(
|
||||
asAnna(),
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
final replacedCode = mailbox.codeFor('anna@example.org');
|
||||
final newer = await endpoints.emailCode.sendCode(
|
||||
asAnna(),
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.emailCode.verifyCode(
|
||||
asAnna(),
|
||||
request: replaced,
|
||||
code: replacedCode,
|
||||
),
|
||||
throwsA(_refusal(EmailSignInRefusal.codeExpired)),
|
||||
);
|
||||
|
||||
final newerCode = mailbox.codeFor('anna@example.org');
|
||||
await endpoints.emailCode.verifyCode(
|
||||
asAnna(),
|
||||
request: newer,
|
||||
code: newerCode,
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.emailCode.verifyCode(
|
||||
asAnna(),
|
||||
request: newer,
|
||||
code: newerCode,
|
||||
),
|
||||
throwsA(_refusal(EmailSignInRefusal.codeExpired)),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'a second address for a person who has one; nothing changes',
|
||||
() async {
|
||||
await expectLater(
|
||||
signInWith(asBen(), 'ben@elsewhere.org'),
|
||||
throwsA(_refusal(EmailSignInRefusal.otherAddressLinked)),
|
||||
);
|
||||
expect(await _addressesOf(session, ben), [
|
||||
addressHash(session.serverpod, 'ben@example.org'),
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
test('a fourth code for one address within an hour, and an '
|
||||
'eleventh from one network', () async {
|
||||
for (var i = 0; i < 3; i++) {
|
||||
await endpoints.emailCode.sendCode(
|
||||
asAnna(),
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
}
|
||||
await expectLater(
|
||||
endpoints.emailCode.sendCode(
|
||||
asAnna(),
|
||||
address: 'Anna@Example.org',
|
||||
language: 'en',
|
||||
),
|
||||
throwsA(_refusal(EmailSignInRefusal.tooManyCodes)),
|
||||
);
|
||||
for (var i = 0; i < 7; i++) {
|
||||
await endpoints.emailCode.sendCode(
|
||||
asAnna(),
|
||||
address: 'anna$i@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
}
|
||||
await expectLater(
|
||||
endpoints.emailCode.sendCode(
|
||||
asAnna(),
|
||||
address: 'anna.else@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
throwsA(_refusal(EmailSignInRefusal.tooManyCodes)),
|
||||
);
|
||||
expect(mailbox.sent, hasLength(10));
|
||||
});
|
||||
|
||||
test('a switch token used twice, or made up', () async {
|
||||
final token = (await signInWith(
|
||||
asAnna(),
|
||||
'ben@example.org',
|
||||
)).switchToken!;
|
||||
await endpoints.emailSignIn.signIn(notSignedIn(), switchToken: token);
|
||||
|
||||
for (final used in [token, 'made-up']) {
|
||||
await expectLater(
|
||||
endpoints.emailSignIn.signIn(notSignedIn(), switchToken: used),
|
||||
throwsA(_refusal(EmailSignInRefusal.codeExpired)),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('saying how someone signs in when not signed in', () async {
|
||||
await expectLater(
|
||||
endpoints.account.signInWays(notSignedIn()),
|
||||
throwsA(isA<ServerpodUnauthenticatedException>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('mails a code in the language asked for, the same whether anybody '
|
||||
'has the address or not', () async {
|
||||
final known = await endpoints.emailCode.sendCode(
|
||||
asAnna(),
|
||||
address: 'ben@example.org',
|
||||
language: 'de',
|
||||
);
|
||||
final unknown = await endpoints.emailCode.sendCode(
|
||||
asAnna(),
|
||||
address: ' Nobody@Example.org ',
|
||||
language: 'fr',
|
||||
);
|
||||
|
||||
expect(known, isNot(unknown));
|
||||
expect(
|
||||
[for (final mail in mailbox.sent) (mail.to, mail.language)],
|
||||
[('ben@example.org', 'de'), ('nobody@example.org', 'en')],
|
||||
);
|
||||
expect(
|
||||
mailbox.sent.map((mail) => mail.code),
|
||||
everyElement(matches(RegExp(r'^[0-9A-Z]{4}-[0-9A-Z]{4}$'))),
|
||||
);
|
||||
});
|
||||
|
||||
test("a code for an address nobody has makes it the person's: they "
|
||||
'become identified, on every session, and stop being anonymous; '
|
||||
'the instance keeps only a keyed hash of it', () async {
|
||||
final request = await endpoints.emailCode.sendCode(
|
||||
asAnna(),
|
||||
address: ' Anna@Example.org',
|
||||
language: 'en',
|
||||
);
|
||||
final code = mailbox.codeFor('anna@example.org');
|
||||
final accepted = await endpoints.emailCode.verifyCode(
|
||||
asAnna(),
|
||||
request: request,
|
||||
// As typed off the mail: lower case, a space for the dash.
|
||||
code: code.toLowerCase().replaceAll('-', ' '),
|
||||
);
|
||||
|
||||
expect(accepted.switchToken, isNull);
|
||||
final hash = addressHash(session.serverpod, 'anna@example.org');
|
||||
expect(hash, matches(RegExp(r'^[0-9a-f]{64}$')));
|
||||
expect(await _addressesOf(session, anna), [hash]);
|
||||
final person = await AuthServices.instance.authUsers.get(
|
||||
session,
|
||||
authUserId: anna,
|
||||
);
|
||||
expect(person.scopeNames, contains(identified.name));
|
||||
expect(
|
||||
(await ServerSideSession.db.findById(
|
||||
session,
|
||||
annasPhone,
|
||||
))?.scopeNames,
|
||||
contains(identified.name),
|
||||
);
|
||||
expect(
|
||||
await AnonymousAccount.db.count(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(anna),
|
||||
),
|
||||
0,
|
||||
);
|
||||
expect(await EmailCodeRequest.db.count(session), 0);
|
||||
final kept = jsonEncode([
|
||||
...await EmailIdentity.db.find(session),
|
||||
...await EmailCodeRequest.db.find(session),
|
||||
...await RateLimitedRequestAttempt.db.find(session),
|
||||
]);
|
||||
expect(kept.toLowerCase(), isNot(contains('anna')));
|
||||
expect(kept, isNot(contains(code)));
|
||||
});
|
||||
|
||||
test('a code for the address the person signs in with changes '
|
||||
'nothing', () async {
|
||||
final accepted = await signInWith(asBen(), 'ben@example.org');
|
||||
|
||||
expect(accepted.switchToken, isNull);
|
||||
expect(await EmailIdentity.db.count(session), 1);
|
||||
});
|
||||
|
||||
test("a code for another person's address changes nothing here, and "
|
||||
'gives a token that signs in as them, without a session', () async {
|
||||
final accepted = await signInWith(asAnna(), 'ben@example.org');
|
||||
|
||||
expect(accepted.switchToken, isNotNull);
|
||||
expect(await _addressesOf(session, anna), isEmpty);
|
||||
expect(
|
||||
await AnonymousAccount.db.count(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(anna),
|
||||
),
|
||||
1,
|
||||
);
|
||||
|
||||
final signedIn = await endpoints.emailSignIn.signIn(
|
||||
notSignedIn(),
|
||||
switchToken: accepted.switchToken!,
|
||||
);
|
||||
expect(signedIn.authUserId, ben);
|
||||
expect(signedIn.scopeNames, contains(identified.name));
|
||||
final sessions = await ServerSideSession.db.find(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(ben) & t.method.equals('email'),
|
||||
);
|
||||
expect(sessions, hasLength(1));
|
||||
expect(await EmailCodeRequest.db.count(session), 0);
|
||||
});
|
||||
|
||||
test('a person who signs in with GitHub adds an address', () async {
|
||||
Future<(bool, bool)> waysOf(TestSessionBuilder person) async {
|
||||
final ways = await endpoints.account.signInWays(person);
|
||||
return (ways.gitHub, ways.email);
|
||||
}
|
||||
|
||||
expect(await waysOf(asCarla()), (true, false));
|
||||
|
||||
final accepted = await signInWith(asCarla(), 'carla@example.org');
|
||||
|
||||
expect(accepted.switchToken, isNull);
|
||||
expect(await waysOf(asCarla()), (true, true));
|
||||
expect(await waysOf(asAnna()), (false, false));
|
||||
});
|
||||
|
||||
test('tells the app that this instance offers it', () async {
|
||||
expect(
|
||||
(await endpoints.instance.basics(notSignedIn())).emailSignIn,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('deleting a person deletes their address', () async {
|
||||
await deletePerson(session, ben);
|
||||
|
||||
expect(await EmailIdentity.db.count(session), 0);
|
||||
});
|
||||
|
||||
test('forgets the codes past their time, hourly', () async {
|
||||
await endpoints.emailCode.sendCode(
|
||||
asAnna(),
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
final now = clock.now();
|
||||
|
||||
expect(await forgetExpiredEmailCodes(session, now: now), 0);
|
||||
expect(
|
||||
await forgetExpiredEmailCodes(
|
||||
session,
|
||||
now: now.add(const Duration(minutes: 16)),
|
||||
),
|
||||
1,
|
||||
);
|
||||
expect(await EmailCodeRequest.db.count(session), 0);
|
||||
expect(await SecretChallenge.db.count(session), 0);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Matcher _refusal(EmailSignInRefusal reason) =>
|
||||
isA<EmailSignInRefusedException>().having(
|
||||
(exception) => exception.reason,
|
||||
'reason',
|
||||
reason,
|
||||
);
|
||||
|
||||
/// The hashes of the addresses [person] signs in with.
|
||||
Future<List<String>> _addressesOf(Session session, UuidValue person) async => [
|
||||
for (final identity in await EmailIdentity.db.find(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(person),
|
||||
))
|
||||
identity.addressHash,
|
||||
];
|
||||
|
||||
/// Deletes every rate-limit record, code, address and person; their
|
||||
/// sessions and other identities go with them (`ON DELETE CASCADE`).
|
||||
Future<void> _deleteAll(Session session) async {
|
||||
await RateLimitedRequestAttempt.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.id.notEquals(null),
|
||||
);
|
||||
await EmailCodeRequest.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.id.notEquals(null),
|
||||
);
|
||||
await SecretChallenge.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.id.notEquals(null),
|
||||
);
|
||||
await EmailIdentity.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.id.notEquals(null),
|
||||
);
|
||||
await AuthUser.db.deleteWhere(session, where: (t) => t.id.notEquals(null));
|
||||
}
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
import 'package:halkyon_base_server/base.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../support/fixtures.dart';
|
||||
import '../test_tools/serverpod_test_tools.dart';
|
||||
|
||||
void main() {
|
||||
withServerpod('GitHub sign-in', (sessionBuilder, endpoints) {
|
||||
late Session session;
|
||||
|
||||
/// Anonymous, signed in on her phone.
|
||||
late UuidValue anna;
|
||||
late UuidValue annasPhone;
|
||||
|
||||
/// Identified, with the GitHub account 1001.
|
||||
late UuidValue ben;
|
||||
|
||||
setUp(() async {
|
||||
useBase(sessionBuilder);
|
||||
session = sessionBuilder.build();
|
||||
anna = await _newAnonymousPerson(session);
|
||||
annasPhone = await signInNewDevice(session, anna);
|
||||
ben = (await AuthServices.instance.authUsers.create(
|
||||
session,
|
||||
scopes: {identified},
|
||||
)).id;
|
||||
await GitHubAccount.db.insertRow(
|
||||
session,
|
||||
GitHubAccount(userIdentifier: '1001', authUserId: ben),
|
||||
);
|
||||
});
|
||||
|
||||
TestSessionBuilder notSignedIn() => sessionBuilder.copyWith(
|
||||
authentication: AuthenticationOverride.unauthenticated(),
|
||||
);
|
||||
|
||||
group('refuses', () {
|
||||
test('upgrading when not signed in', () async {
|
||||
await expectLater(
|
||||
endpoints.gitHubUpgrade.upgrade(
|
||||
notSignedIn(),
|
||||
code: 'code',
|
||||
codeVerifier: 'verifier',
|
||||
redirectUri: _redirectUri,
|
||||
),
|
||||
throwsA(isA<ServerpodUnauthenticatedException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('both ways on an instance without a GitHub OAuth app', () async {
|
||||
// The test configuration has no githubClientId/githubClientSecret.
|
||||
await expectLater(
|
||||
endpoints.gitHubUpgrade.upgrade(
|
||||
sessionBuilder.copyWith(
|
||||
authentication: AuthenticationOverride.authenticationInfo(
|
||||
anna.uuid,
|
||||
{},
|
||||
authId: annasPhone.uuid,
|
||||
),
|
||||
),
|
||||
code: 'code',
|
||||
codeVerifier: 'verifier',
|
||||
redirectUri: _redirectUri,
|
||||
),
|
||||
throwsA(_refusal(GitHubSignInRefusal.unavailable)),
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.gitHubSignIn.signIn(
|
||||
notSignedIn(),
|
||||
code: 'code',
|
||||
codeVerifier: 'verifier',
|
||||
redirectUri: _redirectUri,
|
||||
),
|
||||
throwsA(_refusal(GitHubSignInRefusal.unavailable)),
|
||||
);
|
||||
});
|
||||
|
||||
test('linking a GitHub account that belongs to someone else; nothing '
|
||||
'changes', () async {
|
||||
await expectLater(
|
||||
linkGitHubAccount(session, authUserId: anna, gitHubUserId: '1001'),
|
||||
throwsA(_refusal(GitHubSignInRefusal.takenByAnotherPerson)),
|
||||
);
|
||||
|
||||
expect(await _gitHubAccountsOf(session, anna), isEmpty);
|
||||
expect(await _isAnonymous(session, anna), isTrue);
|
||||
expect(
|
||||
await _scopesOf(session, anna),
|
||||
isNot(contains(identified.name)),
|
||||
);
|
||||
expect(
|
||||
[
|
||||
for (final account in await _gitHubAccountsOf(session, ben))
|
||||
account.userIdentifier,
|
||||
],
|
||||
['1001'],
|
||||
);
|
||||
});
|
||||
|
||||
test('a second GitHub account for the same person', () async {
|
||||
await expectLater(
|
||||
linkGitHubAccount(session, authUserId: ben, gitHubUserId: '2002'),
|
||||
throwsA(_refusal(GitHubSignInRefusal.otherGitHubAccountLinked)),
|
||||
);
|
||||
|
||||
expect(await _gitHubAccount(session, '2002'), isNull);
|
||||
});
|
||||
|
||||
test('signing in with a GitHub account nobody has; nobody is '
|
||||
'created', () async {
|
||||
final people = await AuthUser.db.count(session);
|
||||
|
||||
await expectLater(
|
||||
signInWithGitHubAccount(session, gitHubUserId: '3003'),
|
||||
throwsA(_refusal(GitHubSignInRefusal.unknownAccount)),
|
||||
);
|
||||
|
||||
expect(await AuthUser.db.count(session), people);
|
||||
});
|
||||
});
|
||||
|
||||
test('linking keeps the person and makes them identified, on every '
|
||||
'session', () async {
|
||||
await linkGitHubAccount(session, authUserId: anna, gitHubUserId: '2002');
|
||||
|
||||
final account = await _gitHubAccount(session, '2002');
|
||||
// The same person, so everything they have stays where it is.
|
||||
expect(account?.authUserId, anna);
|
||||
// Only the GitHub ID is kept.
|
||||
expect(account?.email, isNull);
|
||||
expect(await _scopesOf(session, anna), contains(identified.name));
|
||||
expect(
|
||||
(await ServerSideSession.db.findById(session, annasPhone))?.scopeNames,
|
||||
contains(identified.name),
|
||||
);
|
||||
expect(await _isAnonymous(session, anna), isFalse);
|
||||
});
|
||||
|
||||
test('linking the account a person already has changes nothing', () async {
|
||||
await linkGitHubAccount(session, authUserId: ben, gitHubUserId: '1001');
|
||||
|
||||
expect(
|
||||
[
|
||||
for (final account in await _gitHubAccountsOf(session, ben))
|
||||
account.userIdentifier,
|
||||
],
|
||||
['1001'],
|
||||
);
|
||||
});
|
||||
|
||||
test('signing in with a known GitHub account starts a session for that '
|
||||
'person', () async {
|
||||
final authSuccess = await signInWithGitHubAccount(
|
||||
session,
|
||||
gitHubUserId: '1001',
|
||||
);
|
||||
|
||||
expect(authSuccess.authUserId, ben);
|
||||
expect(authSuccess.scopeNames, contains(identified.name));
|
||||
});
|
||||
|
||||
test('tells the app that this instance has no GitHub OAuth app', () async {
|
||||
final info = await endpoints.instance.basics(notSignedIn());
|
||||
|
||||
expect(info.gitHubClientId, isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// The loopback callback the desktop apps use.
|
||||
const _redirectUri = 'http://127.0.0.1:43823/github-callback';
|
||||
|
||||
Matcher _refusal(GitHubSignInRefusal reason) =>
|
||||
isA<GitHubSignInRefusedException>().having(
|
||||
(exception) => exception.reason,
|
||||
'reason',
|
||||
reason,
|
||||
);
|
||||
|
||||
/// A person as the app's first launch creates them: anonymous.
|
||||
Future<UuidValue> _newAnonymousPerson(Session session) async {
|
||||
final person = (await AuthServices.instance.authUsers.create(session)).id;
|
||||
await AnonymousAccount.db.insertRow(
|
||||
session,
|
||||
AnonymousAccount(authUserId: person),
|
||||
);
|
||||
return person;
|
||||
}
|
||||
|
||||
Future<bool> _isAnonymous(Session session, UuidValue person) async =>
|
||||
await AnonymousAccount.db.count(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(person),
|
||||
) >
|
||||
0;
|
||||
|
||||
Future<Set<String>> _scopesOf(Session session, UuidValue person) async =>
|
||||
(await AuthServices.instance.authUsers.get(
|
||||
session,
|
||||
authUserId: person,
|
||||
)).scopeNames;
|
||||
|
||||
Future<List<GitHubAccount>> _gitHubAccountsOf(
|
||||
Session session,
|
||||
UuidValue person,
|
||||
) => GitHubAccount.db.find(session, where: (t) => t.authUserId.equals(person));
|
||||
|
||||
Future<GitHubAccount?> _gitHubAccount(Session session, String gitHubUserId) =>
|
||||
GitHubAccount.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.userIdentifier.equals(gitHubUserId),
|
||||
);
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import 'package:halkyon_base_server/base.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../support/fixtures.dart';
|
||||
import '../test_tools/serverpod_test_tools.dart';
|
||||
|
||||
void main() {
|
||||
withServerpod('grantIdentified', (sessionBuilder, endpoints) {
|
||||
setUp(() => useBase(sessionBuilder));
|
||||
|
||||
test("reaches the person's existing sessions, not only new ones", () async {
|
||||
final session = sessionBuilder.build();
|
||||
final authUsers = AuthServices.instance.authUsers;
|
||||
final person = (await authUsers.create(session)).id;
|
||||
final device = await signInNewDevice(session, person);
|
||||
|
||||
await grantIdentified(session, person);
|
||||
|
||||
final personAfter = await authUsers.get(session, authUserId: person);
|
||||
expect(personAfter.scopeNames, contains(identified.name));
|
||||
final deviceAfter = await ServerSideSession.db.findById(session, device);
|
||||
expect(deviceAfter!.scopeNames, contains(identified.name));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import 'package:test/test.dart';
|
||||
|
||||
import 'test_tools/serverpod_test_tools.dart';
|
||||
|
||||
void main() {
|
||||
withServerpod('A host that never configured the module', (
|
||||
sessionBuilder,
|
||||
endpoints,
|
||||
) {
|
||||
test('is told so, rather than answering with defaults of no one', () async {
|
||||
await expectLater(
|
||||
endpoints.instance.basics(
|
||||
sessionBuilder.copyWith(
|
||||
authentication: AuthenticationOverride.unauthenticated(),
|
||||
),
|
||||
),
|
||||
throwsA(
|
||||
isA<StateError>().having(
|
||||
(error) => error.message,
|
||||
'message',
|
||||
contains('HalkyonBase.configure'),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
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 {}
|
||||
}
|
||||
|
||||
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 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,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import 'package:halkyon_base_server/base.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
|
||||
import '../test_tools/serverpod_test_tools.dart';
|
||||
|
||||
/// The app the tests' host is.
|
||||
const testAppName = 'Halkyon Test';
|
||||
|
||||
/// Where the tests' host says its project publishes its source.
|
||||
const testUpstreamSourceCodeUrl = 'https://code.example.org/halkyon_test';
|
||||
|
||||
/// The oldest app build the tests' host works with.
|
||||
const testMinimumAppBuild = 7;
|
||||
|
||||
/// Sets the module up on the pod [sessionBuilder]'s sessions run on, as a
|
||||
/// host does before its pod starts, with [deletePersonData] as its
|
||||
/// deletions, and authentication exactly as hosts run it
|
||||
/// (`auth_config.dart`). Not on `Serverpod.instance`: that is the pod made
|
||||
/// last, and a test file with several `withServerpod` groups has a pod each.
|
||||
void useBase(
|
||||
TestSessionBuilder sessionBuilder, {
|
||||
List<PersonDataDeletion> deletePersonData = const [],
|
||||
}) {
|
||||
final pod = sessionBuilder.build().serverpod;
|
||||
HalkyonBase.configure(
|
||||
pod,
|
||||
HalkyonBase(
|
||||
appName: testAppName,
|
||||
upstreamSourceCodeUrl: testUpstreamSourceCodeUrl,
|
||||
minimumAppBuild: testMinimumAppBuild,
|
||||
deletePersonData: deletePersonData,
|
||||
),
|
||||
);
|
||||
AuthServices.set(
|
||||
tokenManagerBuilders: tokenManagerBuilders,
|
||||
identityProviderBuilders: identityProviderBuilders(pod),
|
||||
);
|
||||
}
|
||||
|
||||
/// Signs [person] in on a new device and returns the new session's id.
|
||||
Future<UuidValue> signInNewDevice(
|
||||
Session session,
|
||||
UuidValue person, {
|
||||
Set<Scope> scopes = const {},
|
||||
}) async {
|
||||
Future<Set<UuidValue>> sessionIds() async => {
|
||||
for (final serverSideSession in await ServerSideSession.db.find(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(person),
|
||||
))
|
||||
serverSideSession.id!,
|
||||
};
|
||||
|
||||
final before = await sessionIds();
|
||||
await AuthServices.instance.tokenManager.createToken(
|
||||
session,
|
||||
authUserId: person,
|
||||
method: 'test',
|
||||
scopes: scopes,
|
||||
);
|
||||
return (await sessionIds()).difference(before).single;
|
||||
}
|
||||
|
||||
/// A person as an app's first launch creates them: anonymous.
|
||||
Future<UuidValue> newAnonymousPerson(
|
||||
Session session, {
|
||||
DateTime? createdAt,
|
||||
}) async {
|
||||
final person = (await AuthServices.instance.authUsers.create(session)).id;
|
||||
await AnonymousAccount.db.insertRow(
|
||||
session,
|
||||
AnonymousAccount(authUserId: person, createdAt: createdAt),
|
||||
);
|
||||
return person;
|
||||
}
|
||||
|
|
@ -0,0 +1,581 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
// ignore_for_file: no_leading_underscores_for_local_identifiers
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:async' as _ida;
|
||||
import 'dart:io' as _idi;
|
||||
import 'package:halkyon_base_server/src/generated/auth/email_code_accepted.dart'
|
||||
as _i611xxit;
|
||||
import 'package:halkyon_base_server/src/generated/auth/sign_in_ways.dart'
|
||||
as _ik4ah9vo;
|
||||
import 'package:halkyon_base_server/src/generated/instance/instance_basics.dart'
|
||||
as _ihusd8tw;
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
import 'package:serverpod_auth_core_server/serverpod_auth_core_server.dart'
|
||||
as _iacs;
|
||||
import 'package:serverpod_test/serverpod_test.dart' as _ist;
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_base_server/src/generated/endpoints.dart';
|
||||
export 'package:serverpod_test/serverpod_test_public_exports.dart';
|
||||
|
||||
/// Creates a new test group that takes a callback that can be used to write tests.
|
||||
/// The callback has two parameters: `sessionBuilder` and `endpoints`.
|
||||
/// `sessionBuilder` is used to build a `Session` object that represents the server state during an endpoint call and is used to set up scenarios.
|
||||
/// `endpoints` contains all your Serverpod endpoints and lets you call them:
|
||||
/// ```dart
|
||||
/// withServerpod('Given Example endpoint', (sessionBuilder, endpoints) {
|
||||
/// test('when calling `hello` then should return greeting', () async {
|
||||
/// final greeting = await endpoints.example.hello(sessionBuilder, 'Michael');
|
||||
/// expect(greeting, 'Hello Michael');
|
||||
/// });
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// **Configuration options**
|
||||
///
|
||||
/// [applyMigrations] Whether pending migrations should be applied when starting Serverpod. Defaults to `true`
|
||||
///
|
||||
/// [enableSessionLogging] Whether session logging should be enabled. Defaults to `false`
|
||||
///
|
||||
/// [ephemeralDatabase] Whether this group gets its own empty database, created when the group starts and dropped when it finishes. Defaults to `true`.
|
||||
/// Set this to `false` to use the database configured for [runMode], including a previously seeded database. [configOverride] can still replace that database. The configured database is not created or dropped, so groups that share it cannot run in parallel. [rollbackDatabase] still controls whether writes inside the group are rolled back.
|
||||
///
|
||||
/// [rollbackDatabase] Options for when to rollback the database during the test lifecycle.
|
||||
/// By default `withServerpod` does all database operations inside a transaction that is rolled back after each `test` case.
|
||||
/// Just like the following enum describes, the behavior of the automatic rollbacks can be configured:
|
||||
/// ```dart
|
||||
/// /// Options for when to rollback the database during the test lifecycle.
|
||||
/// enum RollbackDatabase {
|
||||
/// /// After each test. This is the default.
|
||||
/// afterEach,
|
||||
///
|
||||
/// /// After all tests.
|
||||
/// afterAll,
|
||||
///
|
||||
/// /// Disable rolling back the database.
|
||||
/// disabled,
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// [runMode] The run mode that Serverpod should be running in. Defaults to `test`.
|
||||
///
|
||||
/// [serverpodLoggingMode] The logging mode used when creating Serverpod. Defaults to `ServerpodLoggingMode.normal`
|
||||
///
|
||||
/// [serverpodStartTimeout] The timeout to use when starting Serverpod, which connects to the database among other things. Defaults to `Duration(seconds: 120)`.
|
||||
///
|
||||
/// [testServerOutputMode] Options for controlling test server output during test execution. Defaults to `TestServerOutputMode.normal`.
|
||||
/// ```dart
|
||||
/// /// Options for controlling test server output during test execution.
|
||||
/// enum TestServerOutputMode {
|
||||
/// /// Default mode - only stderr is printed (stdout suppressed).
|
||||
/// /// This hides normal startup/shutdown logs while preserving error messages.
|
||||
/// normal,
|
||||
///
|
||||
/// /// All logging - both stdout and stderr are printed.
|
||||
/// /// Useful for debugging when you need to see all server output.
|
||||
/// verbose,
|
||||
///
|
||||
/// /// No logging - both stdout and stderr are suppressed.
|
||||
/// /// Completely silent mode, useful when you don't want any server output.
|
||||
/// silent,
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// [configOverride] A function to override the server configuration. This function is called with
|
||||
/// the default server configuration after it is loaded from the config/ directory
|
||||
/// and before it is used to start the server. Use this to override particular
|
||||
/// settings in the server configuration.
|
||||
///
|
||||
/// [databaseInterceptor] Optional interceptor that replaces the default database for each session.
|
||||
/// See [Serverpod.databaseInterceptor] for more information.
|
||||
///
|
||||
/// [testGroupTagsOverride] By default Serverpod test tools tags the `withServerpod` test group with `"integration"`.
|
||||
/// This is to provide a simple way to only run unit or integration tests.
|
||||
/// This property allows this tag to be overridden to something else. Defaults to `['integration']`.
|
||||
///
|
||||
/// [experimentalFeatures] Optionally specify experimental features. See [Serverpod] for more information.
|
||||
///
|
||||
/// [serverDirectory] The server package directory `config/<runMode>.yaml`, `config/passwords.yaml`,
|
||||
/// and `migrations/<module>/...` are resolved against. Defaults to
|
||||
/// [Directory.current] at the time the test boots. Pass this when the test
|
||||
/// isolate's cwd is not the server package root (e.g. running tests from a
|
||||
/// workspace parent directory) so config and migrations are still loaded
|
||||
/// from the right place.
|
||||
@_ist.isTestGroup
|
||||
void withServerpod(
|
||||
String testGroupName,
|
||||
_ist.TestClosure<TestEndpoints> testClosure, {
|
||||
bool? applyMigrations,
|
||||
_is.ServerpodConfig Function(_is.ServerpodConfig)? configOverride,
|
||||
_is.DatabaseInterceptor? databaseInterceptor,
|
||||
bool? enableSessionLogging,
|
||||
bool? ephemeralDatabase,
|
||||
_is.ExperimentalFeatures? experimentalFeatures,
|
||||
_ist.RollbackDatabase? rollbackDatabase,
|
||||
String? runMode,
|
||||
_is.RuntimeParametersListBuilder? runtimeParametersBuilder,
|
||||
_idi.Directory? serverDirectory,
|
||||
_is.ServerpodLoggingMode? serverpodLoggingMode,
|
||||
Duration? serverpodStartTimeout,
|
||||
List<String>? testGroupTagsOverride,
|
||||
_ist.TestServerOutputMode? testServerOutputMode,
|
||||
}) {
|
||||
_ist.buildWithServerpod<_InternalTestEndpoints>(
|
||||
testGroupName,
|
||||
_ist.TestServerpod(
|
||||
testEndpoints: _InternalTestEndpoints(),
|
||||
endpoints: Endpoints(),
|
||||
serializationManager: Protocol(),
|
||||
runMode: runMode,
|
||||
applyMigrations: applyMigrations,
|
||||
ephemeralDatabase: ephemeralDatabase,
|
||||
isDatabaseEnabled: true,
|
||||
serverpodLoggingMode: serverpodLoggingMode,
|
||||
testServerOutputMode: testServerOutputMode,
|
||||
serverDirectory: serverDirectory,
|
||||
experimentalFeatures: experimentalFeatures,
|
||||
configOverride: configOverride,
|
||||
runtimeParametersBuilder: runtimeParametersBuilder,
|
||||
databaseInterceptor: databaseInterceptor,
|
||||
),
|
||||
maybeRollbackDatabase: rollbackDatabase,
|
||||
maybeEnableSessionLogging: enableSessionLogging,
|
||||
maybeTestGroupTagsOverride: testGroupTagsOverride,
|
||||
maybeServerpodStartTimeout: serverpodStartTimeout,
|
||||
maybeTestServerOutputMode: testServerOutputMode,
|
||||
)(testClosure);
|
||||
}
|
||||
|
||||
class TestEndpoints {
|
||||
late final _AccountEndpoint account;
|
||||
|
||||
late final _AnonymousIdpEndpoint anonymousIdp;
|
||||
|
||||
late final _EmailCodeEndpoint emailCode;
|
||||
|
||||
late final _EmailSignInEndpoint emailSignIn;
|
||||
|
||||
late final _GitHubSignInEndpoint gitHubSignIn;
|
||||
|
||||
late final _GitHubUpgradeEndpoint gitHubUpgrade;
|
||||
|
||||
late final _InstanceEndpoint instance;
|
||||
}
|
||||
|
||||
class _InternalTestEndpoints extends TestEndpoints
|
||||
implements _ist.InternalTestEndpoints {
|
||||
@override
|
||||
void initialize(
|
||||
_is.SerializationManager serializationManager,
|
||||
_is.EndpointDispatch endpoints,
|
||||
) {
|
||||
account = _AccountEndpoint(
|
||||
endpoints,
|
||||
serializationManager,
|
||||
);
|
||||
anonymousIdp = _AnonymousIdpEndpoint(
|
||||
endpoints,
|
||||
serializationManager,
|
||||
);
|
||||
emailCode = _EmailCodeEndpoint(
|
||||
endpoints,
|
||||
serializationManager,
|
||||
);
|
||||
emailSignIn = _EmailSignInEndpoint(
|
||||
endpoints,
|
||||
serializationManager,
|
||||
);
|
||||
gitHubSignIn = _GitHubSignInEndpoint(
|
||||
endpoints,
|
||||
serializationManager,
|
||||
);
|
||||
gitHubUpgrade = _GitHubUpgradeEndpoint(
|
||||
endpoints,
|
||||
serializationManager,
|
||||
);
|
||||
instance = _InstanceEndpoint(
|
||||
endpoints,
|
||||
serializationManager,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AccountEndpoint {
|
||||
_AccountEndpoint(
|
||||
this._endpointDispatch,
|
||||
this._serializationManager,
|
||||
);
|
||||
|
||||
final _is.EndpointDispatch _endpointDispatch;
|
||||
|
||||
final _is.SerializationManager _serializationManager;
|
||||
|
||||
_ida.Future<void> delete(_ist.TestSessionBuilder sessionBuilder) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'account',
|
||||
method: 'delete',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'account',
|
||||
methodName: 'delete',
|
||||
parameters: _ist.testObjectToJson({}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<void>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<_ik4ah9vo.SignInWays> signInWays(
|
||||
_ist.TestSessionBuilder sessionBuilder,
|
||||
) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'account',
|
||||
method: 'signInWays',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'account',
|
||||
methodName: 'signInWays',
|
||||
parameters: _ist.testObjectToJson({}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_ik4ah9vo.SignInWays>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _AnonymousIdpEndpoint {
|
||||
_AnonymousIdpEndpoint(
|
||||
this._endpointDispatch,
|
||||
this._serializationManager,
|
||||
);
|
||||
|
||||
final _is.EndpointDispatch _endpointDispatch;
|
||||
|
||||
final _is.SerializationManager _serializationManager;
|
||||
|
||||
_ida.Future<_iacs.AuthSuccess> login(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
String? token,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'anonymousIdp',
|
||||
method: 'login',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'anonymousIdp',
|
||||
methodName: 'login',
|
||||
parameters: _ist.testObjectToJson({'token': token}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_iacs.AuthSuccess>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _EmailCodeEndpoint {
|
||||
_EmailCodeEndpoint(
|
||||
this._endpointDispatch,
|
||||
this._serializationManager,
|
||||
);
|
||||
|
||||
final _is.EndpointDispatch _endpointDispatch;
|
||||
|
||||
final _is.SerializationManager _serializationManager;
|
||||
|
||||
_ida.Future<_is.UuidValue> sendCode(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required String address,
|
||||
required String language,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'emailCode',
|
||||
method: 'sendCode',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'emailCode',
|
||||
methodName: 'sendCode',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'address': address,
|
||||
'language': language,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_is.UuidValue>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<_i611xxit.EmailCodeAccepted> verifyCode(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required _is.UuidValue request,
|
||||
required String code,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'emailCode',
|
||||
method: 'verifyCode',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'emailCode',
|
||||
methodName: 'verifyCode',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'request': request,
|
||||
'code': code,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_i611xxit.EmailCodeAccepted>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _EmailSignInEndpoint {
|
||||
_EmailSignInEndpoint(
|
||||
this._endpointDispatch,
|
||||
this._serializationManager,
|
||||
);
|
||||
|
||||
final _is.EndpointDispatch _endpointDispatch;
|
||||
|
||||
final _is.SerializationManager _serializationManager;
|
||||
|
||||
_ida.Future<_iacs.AuthSuccess> signIn(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required String switchToken,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'emailSignIn',
|
||||
method: 'signIn',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'emailSignIn',
|
||||
methodName: 'signIn',
|
||||
parameters: _ist.testObjectToJson({'switchToken': switchToken}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_iacs.AuthSuccess>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _GitHubSignInEndpoint {
|
||||
_GitHubSignInEndpoint(
|
||||
this._endpointDispatch,
|
||||
this._serializationManager,
|
||||
);
|
||||
|
||||
final _is.EndpointDispatch _endpointDispatch;
|
||||
|
||||
final _is.SerializationManager _serializationManager;
|
||||
|
||||
_ida.Future<_iacs.AuthSuccess> signIn(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required String code,
|
||||
required String codeVerifier,
|
||||
required String redirectUri,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'gitHubSignIn',
|
||||
method: 'signIn',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'gitHubSignIn',
|
||||
methodName: 'signIn',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'code': code,
|
||||
'codeVerifier': codeVerifier,
|
||||
'redirectUri': redirectUri,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_iacs.AuthSuccess>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _GitHubUpgradeEndpoint {
|
||||
_GitHubUpgradeEndpoint(
|
||||
this._endpointDispatch,
|
||||
this._serializationManager,
|
||||
);
|
||||
|
||||
final _is.EndpointDispatch _endpointDispatch;
|
||||
|
||||
final _is.SerializationManager _serializationManager;
|
||||
|
||||
_ida.Future<void> upgrade(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required String code,
|
||||
required String codeVerifier,
|
||||
required String redirectUri,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'gitHubUpgrade',
|
||||
method: 'upgrade',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'gitHubUpgrade',
|
||||
methodName: 'upgrade',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'code': code,
|
||||
'codeVerifier': codeVerifier,
|
||||
'redirectUri': redirectUri,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<void>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _InstanceEndpoint {
|
||||
_InstanceEndpoint(
|
||||
this._endpointDispatch,
|
||||
this._serializationManager,
|
||||
);
|
||||
|
||||
final _is.EndpointDispatch _endpointDispatch;
|
||||
|
||||
final _is.SerializationManager _serializationManager;
|
||||
|
||||
_ida.Future<_ihusd8tw.InstanceBasics> basics(
|
||||
_ist.TestSessionBuilder sessionBuilder,
|
||||
) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'instance',
|
||||
method: 'basics',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'instance',
|
||||
methodName: 'basics',
|
||||
parameters: _ist.testObjectToJson({}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_ihusd8tw.InstanceBasics>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue