import 'dart:async'; import 'package:halkyon_learn_flutter/src/feature/auth/data/auth_gateway.dart'; import 'package:halkyon_learn_flutter/src/feature/auth/model/auth_status.dart'; import 'package:halkyon_learn_flutter/src/feature/auth/model/github_link_outcome.dart'; /// An [AuthGateway] without a server: signing in creates people named /// `anonymous-1`, `anonymous-2`, …, and [signInError] makes it fail. GitHub /// sign-in links the signed-in person, or switches to `github-person`; with /// [gitHubPageOpens], it first waits for [answerGitHub] or a cancel. class FakeAuthGateway({var AuthStatus _status = const SignedOut()}) implements AuthGateway { final _changes = StreamController.broadcast(); /// Thrown by the next sign-ins while set. Exception? signInError; /// How often signing in was attempted. int signIns = 0; /// How often signing out was attempted. int signOuts = 0; /// How often the signed-in person's account was deleted. int accountDeletions = 0; /// Whether the instance offers GitHub sign-in. bool offersGitHub = true; /// How the next GitHub links end. GitHubLinkOutcome gitHubLinkOutcome = GitHubLinkOutcome.linked; /// Thrown by the next GitHub sign-ins while set. Exception? gitHubError; /// How often this device switched to a GitHub person. int gitHubSwitches = 0; /// Whether the app itself waits for the browser, as on Linux and Windows. @override bool canCancelGitHub = false; /// While set, GitHub sign-ins wait as if GitHub's page were open in the /// browser, until [answerGitHub] or [cancelGitHub]. bool gitHubPageOpens = false; /// How often a GitHub sign-in in progress was cancelled. int gitHubCancels = 0; Completer? _gitHubPage; @override AuthStatus get status => _status; @override Stream get statusChanges => _changes.stream; @override Future signInAnonymously() async { signIns++; if (signInError case final error?) { throw error; } _set(SignedIn(authUserId: 'anonymous-$signIns', isIdentified: false)); } @override Future signOut() async { signOuts++; _set(const SignedOut()); } @override Future deleteAccount() async { accountDeletions++; _set(const SignedOut()); } /// Ends the session as when the server revokes this device's session. void revokeSession() => _set(const SignedOut()); @override Future gitHubClientId() async => offersGitHub ? 'github-client-id' : null; @override Future linkGitHub() async { if (gitHubError case final error?) { throw error; } if (!await _authorize()) { return GitHubLinkOutcome.cancelled; } if (gitHubLinkOutcome == GitHubLinkOutcome.linked) { final person = _status as SignedIn; _set(SignedIn(authUserId: person.authUserId, isIdentified: true)); } return gitHubLinkOutcome; } @override Future switchToGitHub() async { if (gitHubError case final error?) { throw error; } if (!await _authorize()) { return false; } gitHubSwitches++; _set(const SignedIn(authUserId: 'github-person', isIdentified: true)); return true; } @override Future cancelGitHub() async { gitHubCancels++; _gitHubPage?.complete(false); _gitHubPage = null; } /// GitHub sends the browser back to the app: the sign-in goes on. void answerGitHub() { _gitHubPage?.complete(true); _gitHubPage = null; } /// Whether GitHub's page was answered rather than cancelled. Future _authorize() => gitHubPageOpens ? (_gitHubPage = Completer()).future : Future.value(true); void _set(AuthStatus status) { _status = status; _changes.add(status); } }