halkyon/packages/halkyon_oidc_flutter/test/oidc_browser_test.dart

184 lines
5.5 KiB
Dart
Raw Normal View History

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);