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 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 Function() comesBack = () => throw UnimplementedError(); int cancelled = 0; @override String get appName => 'My App'; @override Future authenticate({ required String url, required String callbackUrlScheme, required Map options, }) async { waits.add((url: url, callback: callbackUrlScheme)); return await comesBack(); } @override Future cancel() async => cancelled++; @override Future clearAllDanglingCalls() async {} } Matcher _problem(OidcCallbackProblem problem) => isA() .having((error) => error.problem, 'problem', problem);