602 lines
19 KiB
Dart
602 lines
19 KiB
Dart
|
|
import 'package:clock/clock.dart';
|
||
|
|
import 'package:halkyon_base_server/base.dart';
|
||
|
|
import 'package:halkyon_oidc_server/oidc.dart';
|
||
|
|
import 'package:halkyon_oidc_server/testing.dart';
|
||
|
|
import 'package:serverpod/serverpod.dart';
|
||
|
|
import 'package:serverpod_auth_idp_server/core.dart';
|
||
|
|
import 'package:test/test.dart';
|
||
|
|
|
||
|
|
import '../support/fixtures.dart';
|
||
|
|
import '../support/mailbox.dart';
|
||
|
|
import '../support/password_device.dart';
|
||
|
|
import '../support/password_support.dart';
|
||
|
|
import '../test_tools/serverpod_test_tools.dart';
|
||
|
|
|
||
|
|
// Signing in with SimpleLogin on the instance's register (ADR 0020, 3 to 5;
|
||
|
|
// steps 3 and 4 make accounts for newcomers): an attempt begun, signed out
|
||
|
|
// or in, SimpleLogin's code ending it once, a session for the account with
|
||
|
|
// that identity, the identity added to the caller's account, or someone new
|
||
|
|
// whose identity waits half an hour with a handle suggested from their name.
|
||
|
|
// Against a fake SimpleLogin signing real ID tokens (halkyon_oidc's
|
||
|
|
// testing.dart); every way an ID token is refused is the module's test.
|
||
|
|
|
||
|
|
void main() {
|
||
|
|
late TestKey key;
|
||
|
|
|
||
|
|
setUpAll(() => key = TestKey.generate('sl', seed: 11));
|
||
|
|
|
||
|
|
withServerpod(
|
||
|
|
'SimpleLogin on the register',
|
||
|
|
// Sessions issued, and the rate limiter's attempts in transactions of
|
||
|
|
// their own: these tests delete what they make instead.
|
||
|
|
rollbackDatabase: RollbackDatabase.disabled,
|
||
|
|
(sessionBuilder, endpoints) {
|
||
|
|
late Session session;
|
||
|
|
late Serverpod pod;
|
||
|
|
late FakeProvider simpleLogin;
|
||
|
|
|
||
|
|
/// Anonymous, signed in on her phone.
|
||
|
|
late UuidValue anna;
|
||
|
|
late UuidValue annasPhone;
|
||
|
|
|
||
|
|
/// Identified, signing in with SimpleLogin's ID `sl-ben`.
|
||
|
|
late UuidValue ben;
|
||
|
|
|
||
|
|
setUp(() async {
|
||
|
|
useBase(sessionBuilder, app: 'learn');
|
||
|
|
session = sessionBuilder.build();
|
||
|
|
pod = session.serverpod;
|
||
|
|
await _deleteAll(session);
|
||
|
|
simpleLogin = FakeProvider.simpleLogin(
|
||
|
|
key: key,
|
||
|
|
clientId: 'halkyon-learn',
|
||
|
|
clientSecret: 'a secret of the app',
|
||
|
|
callbackUrl: Uri.parse('https://learn.test/simplelogin-callback'),
|
||
|
|
);
|
||
|
|
useSimpleLogin(sessionBuilder, simpleLogin);
|
||
|
|
anna = await newAnonymousPerson(session);
|
||
|
|
annasPhone = await signInNewDevice(session, anna);
|
||
|
|
ben = (await AuthServices.instance.authUsers.create(
|
||
|
|
session,
|
||
|
|
scopes: {identified},
|
||
|
|
)).id;
|
||
|
|
await OidcIdentity.db.insertRow(
|
||
|
|
session,
|
||
|
|
OidcIdentity(
|
||
|
|
authUserId: ben,
|
||
|
|
provider: simpleLoginProvider,
|
||
|
|
subject: 'sl-ben',
|
||
|
|
),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
tearDown(() async {
|
||
|
|
useSimpleLoginApp(pod, null);
|
||
|
|
await _deleteAll(sessionBuilder.build());
|
||
|
|
});
|
||
|
|
|
||
|
|
TestSessionBuilder asAnna() => sessionBuilder.copyWith(
|
||
|
|
authentication: AuthenticationOverride.authenticationInfo(
|
||
|
|
anna.uuid,
|
||
|
|
{},
|
||
|
|
authId: annasPhone.uuid,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
TestSessionBuilder notSignedIn() => sessionBuilder.copyWith(
|
||
|
|
authentication: AuthenticationOverride.unauthenticated(),
|
||
|
|
);
|
||
|
|
|
||
|
|
Matcher refused(OidcRefusal reason) => throwsA(
|
||
|
|
isA<OidcRefusedException>().having((e) => e.reason, 'reason', reason),
|
||
|
|
);
|
||
|
|
|
||
|
|
/// A sign-in begun as [as] and allowed at SimpleLogin by [subject],
|
||
|
|
/// who shares [name]: the attempt and SimpleLogin's code.
|
||
|
|
Future<({UuidValue attempt, String code})> allowed(
|
||
|
|
TestSessionBuilder as, {
|
||
|
|
required String subject,
|
||
|
|
String? name,
|
||
|
|
}) async {
|
||
|
|
final start = await endpoints.simpleLogin.begin(
|
||
|
|
as,
|
||
|
|
platform: SimpleLoginPlatform.desktop,
|
||
|
|
);
|
||
|
|
return (
|
||
|
|
attempt: start.attempt,
|
||
|
|
code: simpleLogin.authorize(
|
||
|
|
start.authorizationUrl,
|
||
|
|
subject: subject,
|
||
|
|
claims: {'name': ?name},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<List<OidcIdentity>> identitiesOf(UuidValue person) =>
|
||
|
|
OidcIdentity.db.find(
|
||
|
|
session,
|
||
|
|
where: (t) => t.authUserId.equals(person),
|
||
|
|
);
|
||
|
|
|
||
|
|
group('is offered', () {
|
||
|
|
test('where the register has its SimpleLogin app', () async {
|
||
|
|
expect(
|
||
|
|
(await endpoints.instance.basics(notSignedIn())).simpleLogin,
|
||
|
|
isTrue,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('not without one, beginning nothing', () async {
|
||
|
|
useSimpleLoginApp(pod, null);
|
||
|
|
useSimpleLogin(sessionBuilder, null);
|
||
|
|
|
||
|
|
expect(
|
||
|
|
(await endpoints.instance.basics(notSignedIn())).simpleLogin,
|
||
|
|
isFalse,
|
||
|
|
);
|
||
|
|
await expectLater(
|
||
|
|
endpoints.simpleLogin.begin(
|
||
|
|
notSignedIn(),
|
||
|
|
platform: SimpleLoginPlatform.desktop,
|
||
|
|
),
|
||
|
|
refused(OidcRefusal.unknownProvider),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('not where the operator offers passwords alone, nor ends what '
|
||
|
|
'was begun before', () async {
|
||
|
|
final (:attempt, :code) = await allowed(
|
||
|
|
notSignedIn(),
|
||
|
|
subject: 'sl-ben',
|
||
|
|
);
|
||
|
|
useOfferedWays(pod, {OfferedWay.password});
|
||
|
|
|
||
|
|
expect(
|
||
|
|
(await endpoints.instance.basics(notSignedIn())).simpleLogin,
|
||
|
|
isFalse,
|
||
|
|
);
|
||
|
|
await expectLater(
|
||
|
|
endpoints.simpleLogin.begin(
|
||
|
|
notSignedIn(),
|
||
|
|
platform: SimpleLoginPlatform.desktop,
|
||
|
|
),
|
||
|
|
refused(OidcRefusal.unknownProvider),
|
||
|
|
);
|
||
|
|
await expectLater(
|
||
|
|
endpoints.simpleLogin.complete(
|
||
|
|
notSignedIn(),
|
||
|
|
attempt: attempt,
|
||
|
|
code: code,
|
||
|
|
),
|
||
|
|
refused(OidcRefusal.unknownProvider),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('alone where the operator offers it alone: no password sign-in, '
|
||
|
|
'its keys there or not', () async {
|
||
|
|
EmailCodes.of(pod).mailer = Mailbox();
|
||
|
|
usePasswordKeys(pod, testPasswordKeys);
|
||
|
|
addTearDown(() {
|
||
|
|
EmailCodes.of(pod).mailer = null;
|
||
|
|
usePasswordKeys(pod, null);
|
||
|
|
});
|
||
|
|
expect(
|
||
|
|
(await endpoints.instance.basics(notSignedIn())).passwordSignIn,
|
||
|
|
isTrue,
|
||
|
|
);
|
||
|
|
|
||
|
|
useOfferedWays(pod, {OfferedWay.simpleLogin});
|
||
|
|
|
||
|
|
final basics = await endpoints.instance.basics(notSignedIn());
|
||
|
|
expect(basics.passwordSignIn, isFalse);
|
||
|
|
expect(basics.simpleLogin, isTrue);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
group('beginning', () {
|
||
|
|
test('sends the person to SimpleLogin, the state marked with the app '
|
||
|
|
'and its platform for the forwarding page', () async {
|
||
|
|
final desktop = await endpoints.simpleLogin.begin(
|
||
|
|
notSignedIn(),
|
||
|
|
platform: SimpleLoginPlatform.desktop,
|
||
|
|
);
|
||
|
|
final phone = await endpoints.simpleLogin.begin(
|
||
|
|
notSignedIn(),
|
||
|
|
platform: SimpleLoginPlatform.phone,
|
||
|
|
);
|
||
|
|
|
||
|
|
final url = desktop.authorizationUrl;
|
||
|
|
expect(
|
||
|
|
'${url.origin}${url.path}',
|
||
|
|
'https://app.simplelogin.io/oauth2/authorize',
|
||
|
|
);
|
||
|
|
expect(url.queryParameters['client_id'], 'halkyon-learn');
|
||
|
|
expect(
|
||
|
|
url.queryParameters['redirect_uri'],
|
||
|
|
'https://learn.test/simplelogin-callback',
|
||
|
|
);
|
||
|
|
expect(desktop.state, startsWith('learn-desktop.'));
|
||
|
|
expect(phone.state, startsWith('learn-phone.'));
|
||
|
|
expect(
|
||
|
|
(await OidcAttempt.db.findById(session, desktop.attempt))!.callerId,
|
||
|
|
isNull,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('signed in, notes who began it', () async {
|
||
|
|
final start = await endpoints.simpleLogin.begin(
|
||
|
|
asAnna(),
|
||
|
|
platform: SimpleLoginPlatform.phone,
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(
|
||
|
|
(await OidcAttempt.db.findById(session, start.attempt))!.callerId,
|
||
|
|
anna,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('for an app the forwarding page has no way back to is its '
|
||
|
|
"host's mistake", () async {
|
||
|
|
// The tests' host, which the forwarding page knows nothing of.
|
||
|
|
useBase(sessionBuilder);
|
||
|
|
useSimpleLogin(sessionBuilder, simpleLogin);
|
||
|
|
|
||
|
|
await expectLater(
|
||
|
|
endpoints.simpleLogin.begin(
|
||
|
|
notSignedIn(),
|
||
|
|
platform: SimpleLoginPlatform.desktop,
|
||
|
|
),
|
||
|
|
throwsA(isA<StateError>()),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('at most 30 times an hour from one network', () async {
|
||
|
|
for (var i = 0; i < 30; i++) {
|
||
|
|
await endpoints.simpleLogin.begin(
|
||
|
|
notSignedIn(),
|
||
|
|
platform: SimpleLoginPlatform.desktop,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
await expectLater(
|
||
|
|
endpoints.simpleLogin.begin(
|
||
|
|
notSignedIn(),
|
||
|
|
platform: SimpleLoginPlatform.desktop,
|
||
|
|
),
|
||
|
|
refused(OidcRefusal.tooManyAttempts),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
group('signed out', () {
|
||
|
|
test('signs in to the account with that identity, a session of '
|
||
|
|
"SimpleLogin's", () async {
|
||
|
|
final (:attempt, :code) = await allowed(
|
||
|
|
notSignedIn(),
|
||
|
|
subject: 'sl-ben',
|
||
|
|
name: 'Ben Berg',
|
||
|
|
);
|
||
|
|
|
||
|
|
final completed = await endpoints.simpleLogin.complete(
|
||
|
|
notSignedIn(),
|
||
|
|
attempt: attempt,
|
||
|
|
code: code,
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(completed.outcome, SimpleLoginOutcome.signedIn);
|
||
|
|
expect(completed.suggestedHandle, isNull);
|
||
|
|
expect(completed.session!.authUserId, ben);
|
||
|
|
final sessions = await ServerSideSession.db.find(
|
||
|
|
session,
|
||
|
|
where: (t) => t.authUserId.equals(ben),
|
||
|
|
);
|
||
|
|
expect(sessions.single.method, simpleLoginMethod);
|
||
|
|
expect(sessions.single.scopeNames, contains(identified.name));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('someone new gets no account yet: their identity waits half an '
|
||
|
|
'hour, a handle suggested from their name', () async {
|
||
|
|
final people = await AuthUser.db.count(session);
|
||
|
|
final now = DateTime.now().toUtc();
|
||
|
|
final (:attempt, :code) = await allowed(
|
||
|
|
notSignedIn(),
|
||
|
|
subject: 'sl-carla',
|
||
|
|
name: 'Carla Müller',
|
||
|
|
);
|
||
|
|
|
||
|
|
final completed = await withClock(
|
||
|
|
Clock.fixed(now),
|
||
|
|
() => endpoints.simpleLogin.complete(
|
||
|
|
notSignedIn(),
|
||
|
|
attempt: attempt,
|
||
|
|
code: code,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(completed.outcome, SimpleLoginOutcome.newcomer);
|
||
|
|
expect(completed.session, isNull);
|
||
|
|
expect(completed.suggestedHandle, 'carla_mueller');
|
||
|
|
expect(await AuthUser.db.count(session), people);
|
||
|
|
expect(await OidcIdentity.db.count(session), 1);
|
||
|
|
final kept = (await OidcAttempt.db.findById(session, attempt))!;
|
||
|
|
expect(kept.subject, 'sl-carla');
|
||
|
|
expect(kept.expiresAt, now.add(const Duration(minutes: 30)));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('the handle suggested is one nobody has, whatever its case, and '
|
||
|
|
'none without a name', () async {
|
||
|
|
await AccountRegister.of(pod).saveProfile(
|
||
|
|
session,
|
||
|
|
ben,
|
||
|
|
handle: 'Carla_Mueller',
|
||
|
|
displayName: 'Ben',
|
||
|
|
bio: '',
|
||
|
|
links: const [],
|
||
|
|
);
|
||
|
|
|
||
|
|
final taken = await allowed(
|
||
|
|
notSignedIn(),
|
||
|
|
subject: 'sl-carla',
|
||
|
|
name: 'Carla Müller',
|
||
|
|
);
|
||
|
|
final nameless = await allowed(notSignedIn(), subject: 'sl-dora');
|
||
|
|
|
||
|
|
expect(
|
||
|
|
(await endpoints.simpleLogin.complete(
|
||
|
|
notSignedIn(),
|
||
|
|
attempt: taken.attempt,
|
||
|
|
code: taken.code,
|
||
|
|
)).suggestedHandle,
|
||
|
|
'carla_mueller2',
|
||
|
|
);
|
||
|
|
final completed = await endpoints.simpleLogin.complete(
|
||
|
|
notSignedIn(),
|
||
|
|
attempt: nameless.attempt,
|
||
|
|
code: nameless.code,
|
||
|
|
);
|
||
|
|
expect(completed.outcome, SimpleLoginOutcome.newcomer);
|
||
|
|
expect(completed.suggestedHandle, isNull);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('ends an attempt once', () async {
|
||
|
|
final (:attempt, :code) = await allowed(
|
||
|
|
notSignedIn(),
|
||
|
|
subject: 'sl-ben',
|
||
|
|
);
|
||
|
|
await endpoints.simpleLogin.complete(
|
||
|
|
notSignedIn(),
|
||
|
|
attempt: attempt,
|
||
|
|
code: code,
|
||
|
|
);
|
||
|
|
|
||
|
|
await expectLater(
|
||
|
|
endpoints.simpleLogin.complete(
|
||
|
|
notSignedIn(),
|
||
|
|
attempt: attempt,
|
||
|
|
code: code,
|
||
|
|
),
|
||
|
|
refused(OidcRefusal.unknownAttempt),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('refuses an attempt begun signed in, signing nobody in', () async {
|
||
|
|
final (:attempt, :code) = await allowed(asAnna(), subject: 'sl-ben');
|
||
|
|
|
||
|
|
await expectLater(
|
||
|
|
endpoints.simpleLogin.complete(
|
||
|
|
notSignedIn(),
|
||
|
|
attempt: attempt,
|
||
|
|
code: code,
|
||
|
|
),
|
||
|
|
refused(OidcRefusal.wrongCaller),
|
||
|
|
);
|
||
|
|
expect(
|
||
|
|
await ServerSideSession.db.count(
|
||
|
|
session,
|
||
|
|
where: (t) => t.authUserId.equals(ben),
|
||
|
|
),
|
||
|
|
0,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('refuses a code SimpleLogin refuses, and an attempt whose ten '
|
||
|
|
'minutes are over', () async {
|
||
|
|
final first = await allowed(notSignedIn(), subject: 'sl-ben');
|
||
|
|
final second = await allowed(notSignedIn(), subject: 'sl-ben');
|
||
|
|
|
||
|
|
await expectLater(
|
||
|
|
endpoints.simpleLogin.complete(
|
||
|
|
notSignedIn(),
|
||
|
|
attempt: first.attempt,
|
||
|
|
code: second.code,
|
||
|
|
),
|
||
|
|
refused(OidcRefusal.rejected),
|
||
|
|
);
|
||
|
|
await expectLater(
|
||
|
|
withClock(
|
||
|
|
Clock.fixed(DateTime.now().add(const Duration(minutes: 11))),
|
||
|
|
() => endpoints.simpleLogin.complete(
|
||
|
|
notSignedIn(),
|
||
|
|
attempt: second.attempt,
|
||
|
|
code: second.code,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
refused(OidcRefusal.unknownAttempt),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
group('signed in', () {
|
||
|
|
test('adds SimpleLogin to the caller, who becomes identified and is '
|
||
|
|
'anonymous no longer', () async {
|
||
|
|
final (:attempt, :code) = await allowed(asAnna(), subject: 'sl-anna');
|
||
|
|
|
||
|
|
final completed = await endpoints.simpleLogin.complete(
|
||
|
|
asAnna(),
|
||
|
|
attempt: attempt,
|
||
|
|
code: code,
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(completed.outcome, SimpleLoginOutcome.linked);
|
||
|
|
expect(completed.session, isNull);
|
||
|
|
expect((await identitiesOf(anna)).single.subject, 'sl-anna');
|
||
|
|
expect(
|
||
|
|
(await endpoints.account.signInWays(asAnna())).simpleLogin,
|
||
|
|
isTrue,
|
||
|
|
);
|
||
|
|
expect(
|
||
|
|
(await AuthUser.db.findById(session, anna))!.scopeNames,
|
||
|
|
contains(identified.name),
|
||
|
|
);
|
||
|
|
expect(
|
||
|
|
await AnonymousAccount.db.count(
|
||
|
|
session,
|
||
|
|
where: (t) => t.authUserId.equals(anna),
|
||
|
|
),
|
||
|
|
0,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('the identity they have already changes nothing', () async {
|
||
|
|
final first = await allowed(asAnna(), subject: 'sl-anna');
|
||
|
|
await endpoints.simpleLogin.complete(
|
||
|
|
asAnna(),
|
||
|
|
attempt: first.attempt,
|
||
|
|
code: first.code,
|
||
|
|
);
|
||
|
|
final again = await allowed(asAnna(), subject: 'sl-anna');
|
||
|
|
|
||
|
|
expect(
|
||
|
|
(await endpoints.simpleLogin.complete(
|
||
|
|
asAnna(),
|
||
|
|
attempt: again.attempt,
|
||
|
|
code: again.code,
|
||
|
|
)).outcome,
|
||
|
|
SimpleLoginOutcome.linked,
|
||
|
|
);
|
||
|
|
expect(await identitiesOf(anna), hasLength(1));
|
||
|
|
});
|
||
|
|
|
||
|
|
test("refuses another person's identity, switching nobody and "
|
||
|
|
'changing nothing', () async {
|
||
|
|
final (:attempt, :code) = await allowed(asAnna(), subject: 'sl-ben');
|
||
|
|
|
||
|
|
await expectLater(
|
||
|
|
endpoints.simpleLogin.complete(
|
||
|
|
asAnna(),
|
||
|
|
attempt: attempt,
|
||
|
|
code: code,
|
||
|
|
),
|
||
|
|
refused(OidcRefusal.takenByAnotherPerson),
|
||
|
|
);
|
||
|
|
expect(await identitiesOf(anna), isEmpty);
|
||
|
|
expect(
|
||
|
|
await AnonymousAccount.db.count(
|
||
|
|
session,
|
||
|
|
where: (t) => t.authUserId.equals(anna),
|
||
|
|
),
|
||
|
|
1,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('refuses a second SimpleLogin identity', () async {
|
||
|
|
final first = await allowed(asAnna(), subject: 'sl-anna');
|
||
|
|
await endpoints.simpleLogin.complete(
|
||
|
|
asAnna(),
|
||
|
|
attempt: first.attempt,
|
||
|
|
code: first.code,
|
||
|
|
);
|
||
|
|
final other = await allowed(asAnna(), subject: 'sl-anna-2');
|
||
|
|
|
||
|
|
await expectLater(
|
||
|
|
endpoints.simpleLogin.complete(
|
||
|
|
asAnna(),
|
||
|
|
attempt: other.attempt,
|
||
|
|
code: other.code,
|
||
|
|
),
|
||
|
|
refused(OidcRefusal.otherIdentityLinked),
|
||
|
|
);
|
||
|
|
expect((await identitiesOf(anna)).single.subject, 'sl-anna');
|
||
|
|
});
|
||
|
|
|
||
|
|
test("refuses an attempt begun signed out, or by someone else's "
|
||
|
|
'session', () async {
|
||
|
|
final signedOut = await allowed(notSignedIn(), subject: 'sl-anna');
|
||
|
|
|
||
|
|
await expectLater(
|
||
|
|
endpoints.simpleLogin.complete(
|
||
|
|
asAnna(),
|
||
|
|
attempt: signedOut.attempt,
|
||
|
|
code: signedOut.code,
|
||
|
|
),
|
||
|
|
refused(OidcRefusal.wrongCaller),
|
||
|
|
);
|
||
|
|
expect(await identitiesOf(anna), isEmpty);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test('the identity goes with the account', () async {
|
||
|
|
final (:attempt, :code) = await allowed(asAnna(), subject: 'sl-anna');
|
||
|
|
await endpoints.simpleLogin.complete(
|
||
|
|
asAnna(),
|
||
|
|
attempt: attempt,
|
||
|
|
code: code,
|
||
|
|
);
|
||
|
|
|
||
|
|
await deletePerson(session, anna);
|
||
|
|
|
||
|
|
expect(await identitiesOf(anna), isEmpty);
|
||
|
|
expect(await identitiesOf(ben), hasLength(1));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('no new GitHub account is linked where SimpleLogin is offered: '
|
||
|
|
'GitHub is deprecated', () async {
|
||
|
|
await expectLater(
|
||
|
|
linkGitHubAccount(session, authUserId: anna, gitHubUserId: '2002'),
|
||
|
|
throwsA(
|
||
|
|
isA<GitHubSignInRefusedException>().having(
|
||
|
|
(e) => e.reason,
|
||
|
|
'reason',
|
||
|
|
GitHubSignInRefusal.deprecated,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("forgets the attempts whose time ran out, a newcomer's too, and "
|
||
|
|
'says how many', () async {
|
||
|
|
await allowed(notSignedIn(), subject: 'sl-ben');
|
||
|
|
final (:attempt, :code) = await allowed(
|
||
|
|
notSignedIn(),
|
||
|
|
subject: 'sl-carla',
|
||
|
|
);
|
||
|
|
await endpoints.simpleLogin.complete(
|
||
|
|
notSignedIn(),
|
||
|
|
attempt: attempt,
|
||
|
|
code: code,
|
||
|
|
);
|
||
|
|
|
||
|
|
final now = DateTime.now();
|
||
|
|
expect(
|
||
|
|
await forgetUnfinishedSimpleLogins(
|
||
|
|
session,
|
||
|
|
now: now.add(const Duration(minutes: 11)),
|
||
|
|
),
|
||
|
|
1,
|
||
|
|
);
|
||
|
|
expect(
|
||
|
|
await forgetUnfinishedSimpleLogins(
|
||
|
|
session,
|
||
|
|
now: now.add(const Duration(minutes: 31)),
|
||
|
|
),
|
||
|
|
1,
|
||
|
|
);
|
||
|
|
expect(await OidcAttempt.db.count(session), 0);
|
||
|
|
});
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<void> _deleteAll(Session session) async {
|
||
|
|
await OidcAttempt.db.deleteWhere(session, where: (t) => t.id.notEquals(null));
|
||
|
|
await PublicProfile.db.deleteWhere(session, where: (t) => t.id > 0);
|
||
|
|
await deleteAll(session);
|
||
|
|
}
|