184 lines
5.5 KiB
Dart
184 lines
5.5 KiB
Dart
|
|
import 'dart:async';
|
||
|
|
|
||
|
|
import 'package:flutter/services.dart';
|
||
|
|
import 'package:flutter_test/flutter_test.dart';
|
||
|
|
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
|
||
|
|
import 'package:halkyon_oidc_flutter/halkyon_oidc_flutter.dart';
|
||
|
|
|
||
|
|
void main() {
|
||
|
|
const state = 'app-scheme.s1';
|
||
|
|
final start = OidcStart(
|
||
|
|
attempt: UuidValue.fromString('5b9d7f2e-0c1a-4e3b-9f6d-2a8c4e1b7d30'),
|
||
|
|
authorizationUrl: Uri.parse(
|
||
|
|
'https://id.example.org/authorize?client_id=app&state=$state',
|
||
|
|
),
|
||
|
|
state: state,
|
||
|
|
);
|
||
|
|
|
||
|
|
late List<({String url, String callback, FlutterWebAuth2Options options})>
|
||
|
|
opened;
|
||
|
|
late FutureOr<String> Function() comesBack;
|
||
|
|
late _FakeLoopback loopback;
|
||
|
|
late OidcBrowser browser;
|
||
|
|
|
||
|
|
setUp(() {
|
||
|
|
opened = [];
|
||
|
|
comesBack = () => 'my.app://oidc-callback?code=c&state=$state';
|
||
|
|
loopback = _FakeLoopback();
|
||
|
|
browser = OidcBrowser(
|
||
|
|
loopback: loopback,
|
||
|
|
authenticate:
|
||
|
|
({
|
||
|
|
required url,
|
||
|
|
required callbackUrlScheme,
|
||
|
|
options = const FlutterWebAuth2Options(),
|
||
|
|
}) async {
|
||
|
|
opened.add((
|
||
|
|
url: url,
|
||
|
|
callback: callbackUrlScheme,
|
||
|
|
options: options,
|
||
|
|
));
|
||
|
|
return await comesBack();
|
||
|
|
},
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
group('by the scheme', () {
|
||
|
|
test("opens the provider's page and returns the code", () async {
|
||
|
|
final back = await browser.open(start, const OidcReturn.scheme('my.app'));
|
||
|
|
|
||
|
|
expect(back, const OidcCode('c'));
|
||
|
|
expect(opened.single.url, start.authorizationUrl.toString());
|
||
|
|
expect(opened.single.callback, 'my.app');
|
||
|
|
expect(loopback.waits, isEmpty);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("returns the provider's refusal", () async {
|
||
|
|
comesBack = () => 'my.app://oidc-callback?error=deny&state=$state';
|
||
|
|
|
||
|
|
expect(
|
||
|
|
await browser.open(start, const OidcReturn.scheme('my.app')),
|
||
|
|
const OidcDenied('deny'),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('returns nothing when the person closed the browser', () async {
|
||
|
|
comesBack = () => throw PlatformException(code: 'CANCELED');
|
||
|
|
|
||
|
|
expect(
|
||
|
|
await browser.open(start, const OidcReturn.scheme('my.app')),
|
||
|
|
isNull,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test('by https, names the host and the path the system catches', () async {
|
||
|
|
comesBack = () => 'https://example.org/oidc-callback?code=c&state=$state';
|
||
|
|
|
||
|
|
final back = await browser.open(
|
||
|
|
start,
|
||
|
|
const OidcReturn.https(host: 'example.org', path: '/oidc-callback'),
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(back, const OidcCode('c'));
|
||
|
|
expect(opened.single.callback, 'https');
|
||
|
|
expect(opened.single.options.httpsHost, 'example.org');
|
||
|
|
expect(opened.single.options.httpsPath, '/oidc-callback');
|
||
|
|
});
|
||
|
|
|
||
|
|
group('by the loopback', () {
|
||
|
|
const back = OidcReturn.loopback(port: 43823, path: '/oidc-callback');
|
||
|
|
|
||
|
|
test('waits on the loopback address, not through the package', () async {
|
||
|
|
loopback.comesBack = () =>
|
||
|
|
'http://127.0.0.1:43823/oidc-callback?code=c&state=$state';
|
||
|
|
|
||
|
|
expect(await browser.open(start, back), const OidcCode('c'));
|
||
|
|
expect(loopback.waits.single, (
|
||
|
|
url: start.authorizationUrl.toString(),
|
||
|
|
callback: 'http://127.0.0.1:43823/oidc-callback',
|
||
|
|
));
|
||
|
|
expect(opened, isEmpty);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('returns nothing once the wait is cancelled or runs out', () async {
|
||
|
|
for (final error in [
|
||
|
|
PlatformException(code: 'CANCELED'),
|
||
|
|
TimeoutException('5 minutes'),
|
||
|
|
]) {
|
||
|
|
loopback.comesBack = () => throw error;
|
||
|
|
|
||
|
|
expect(await browser.open(start, back), isNull, reason: '$error');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('cancel stops the wait', () async {
|
||
|
|
await browser.cancel();
|
||
|
|
|
||
|
|
expect(loopback.cancelled, 1);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test('refuses what came back for another sign-in', () async {
|
||
|
|
comesBack = () => 'my.app://oidc-callback?code=c&state=app-scheme.other';
|
||
|
|
|
||
|
|
await expectLater(
|
||
|
|
browser.open(start, const OidcReturn.scheme('my.app')),
|
||
|
|
throwsA(_problem(OidcCallbackProblem.wrongState)),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("says when there's no browser to open the page in", () async {
|
||
|
|
for (final code in ['NO_BROWSER', 'SECURITY_EXCEPTION']) {
|
||
|
|
comesBack = () => throw PlatformException(code: code);
|
||
|
|
|
||
|
|
await expectLater(
|
||
|
|
browser.open(start, const OidcReturn.scheme('my.app')),
|
||
|
|
throwsA(_problem(OidcCallbackProblem.noBrowser)),
|
||
|
|
reason: code,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('says when the way back failed otherwise', () async {
|
||
|
|
for (final code in ['FAILED', 'CANNOT_RESTORE', 'UNKNOWN']) {
|
||
|
|
comesBack = () => throw PlatformException(code: code);
|
||
|
|
|
||
|
|
await expectLater(
|
||
|
|
browser.open(start, const OidcReturn.scheme('my.app')),
|
||
|
|
throwsA(_problem(OidcCallbackProblem.failed)),
|
||
|
|
reason: code,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The loopback with no port and no browser: records what it is asked to
|
||
|
|
/// wait for and answers with [comesBack].
|
||
|
|
class _FakeLoopback() implements LoopbackWebAuth {
|
||
|
|
final waits = <({String url, String callback})>[];
|
||
|
|
FutureOr<String> Function() comesBack = () => throw UnimplementedError();
|
||
|
|
int cancelled = 0;
|
||
|
|
|
||
|
|
@override
|
||
|
|
String get appName => 'My App';
|
||
|
|
|
||
|
|
@override
|
||
|
|
Future<String> authenticate({
|
||
|
|
required String url,
|
||
|
|
required String callbackUrlScheme,
|
||
|
|
required Map<String, dynamic> options,
|
||
|
|
}) async {
|
||
|
|
waits.add((url: url, callback: callbackUrlScheme));
|
||
|
|
return await comesBack();
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
Future<void> cancel() async => cancelled++;
|
||
|
|
|
||
|
|
@override
|
||
|
|
Future<void> clearAllDanglingCalls() async {}
|
||
|
|
}
|
||
|
|
|
||
|
|
Matcher _problem(OidcCallbackProblem problem) => isA<OidcCallbackException>()
|
||
|
|
.having((error) => error.problem, 'problem', problem);
|