import 'package:flutter_test/flutter_test.dart'; import 'package:halkyon_app/halkyon_app.dart'; import 'package:halkyon_app/testing.dart'; void main() { late FakeAuthGateway gateway; late SessionBloc bloc; /// An anonymous account's session, left from before they went (ADR 0020, /// D25). const anonymous = SignedIn(authUserId: 'anonymous-1', isIdentified: false); void start({AuthStatus status = const SignedOut()}) { gateway = FakeAuthGateway(status: status); bloc = SessionBloc(AuthRepository(gateway, MemorySignInAddresses())); } tearDown(() => bloc.close()); test('nobody signed in, it asks how to start, making no account (ADR ' '0020, D4)', () async { start(); bloc.add(const SessionEventStarted()); await expectLater(bloc.stream, emits(const SessionStateSignedOut())); expect(gateway.accountDeletions, 0); expect(gateway.signOuts, 0); }); test('keeps a stored session', () async { const stored = SignedIn(authUserId: 'stored', isIdentified: true); start(status: stored); bloc.add(const SessionEventStarted()); await expectLater(bloc.stream, emits(const SessionStateActive(stored))); }); test( "an anonymous account's session left from before ends as the app " 'starts: the account deleted, the device asked how to start (D25)', () async { start(status: anonymous); bloc.add(const SessionEventStarted()); await expectLater(bloc.stream, emits(const SessionStateSignedOut())); expect(gateway.accountDeletions, 1); expect(gateway.status, const SignedOut()); }, ); test('the instance out of reach, the anonymous session still ends on the ' "device: the account is left to the instance's pruning", () async { start(status: anonymous); gateway.deletionError = Exception('No connection'); bloc.add(const SessionEventStarted()); await expectLater(bloc.stream, emits(const SessionStateSignedOut())); expect(gateway.signOuts, 1); expect(gateway.status, const SignedOut()); }); test("an anonymous session the device can't even end never opens its " 'app: the start screen all the same', () async { start(status: anonymous); gateway ..deletionError = Exception('No connection') ..signOutError = Exception('The keyring is locked'); bloc.add(const SessionEventStarted()); await expectLater(bloc.stream, emits(const SessionStateSignedOut())); expect(gateway.status, anonymous); }); test('after signing out, asks how to start, deleting nothing', () async { const stored = SignedIn(authUserId: 'stored', isIdentified: true); start(status: stored); bloc.add(const SessionEventStarted()); await expectLater(bloc.stream, emits(const SessionStateActive(stored))); bloc.add(const SessionEventSignOutRequested()); // The person's app stays while signing out, never the launch screen in // between. await expectLater( bloc.stream, emitsInOrder([ const SessionStateActive(stored, busy: SessionAction.signOut), const SessionStateSignedOut(), ]), ); expect(gateway.accountDeletions, 0); }); test('signing in with GitHub from there switches to its person', () async { start(); bloc.add(const SessionEventStarted()); await expectLater(bloc.stream, emits(const SessionStateSignedOut())); bloc.add(const SessionEventGitHubRequested()); // A password is offered first (ADR 0017, D2), then the person's app. const person = SignedIn(authUserId: 'github-person', isIdentified: true); await expectLater( bloc.stream, emitsThrough(const SessionStateGitHubSignedIn(person)), ); bloc.add(const SessionEventPasswordOfferDone()); await expectLater(bloc.stream, emits(const SessionStateActive(person))); }); test('a GitHub account nobody here signs in with yet is said so; any ' 'other refusal fails, and nothing is made either way', () async { start(); bloc.add(const SessionEventStarted()); await expectLater(bloc.stream, emits(const SessionStateSignedOut())); gateway.gitHubError = const GitHubSignInFailed( GitHubSignInProblem.unknownAccount, ); bloc.add(const SessionEventGitHubRequested()); await expectLater( bloc.stream, emitsInOrder([ const SessionStateSignedOut(busy: true), const SessionStateSignedOut(problem: SignedOutProblem.noGitHubAccount), ]), ); gateway.gitHubError = const GitHubSignInFailed(GitHubSignInProblem.refused); bloc.add(const SessionEventGitHubRequested()); await expectLater( bloc.stream, emitsInOrder([ const SessionStateSignedOut(busy: true), const SessionStateSignedOut(problem: SignedOutProblem.failed), ]), ); expect(gateway.gitHubSwitches, 0); }); test('an account that signs in with its password now, and GitHub having ' 'ended, are each said so, signing nobody in', () async { start(); bloc.add(const SessionEventStarted()); await expectLater(bloc.stream, emits(const SessionStateSignedOut())); gateway.gitHubError = const GitHubSignInFailed( GitHubSignInProblem.usePassword, ); bloc.add(const SessionEventGitHubRequested()); await expectLater( bloc.stream, emitsInOrder([ const SessionStateSignedOut(busy: true), const SessionStateSignedOut(problem: SignedOutProblem.usePassword), ]), ); gateway.gitHubError = const GitHubSignInFailed(GitHubSignInProblem.ended); bloc.add(const SessionEventGitHubRequested()); await expectLater( bloc.stream, emitsInOrder([ const SessionStateSignedOut(busy: true), const SessionStateSignedOut(problem: SignedOutProblem.gitHubEnded), ]), ); expect(gateway.gitHubSwitches, 0); }); test('a session the server ended asks how to start too', () async { const stored = SignedIn(authUserId: 'github-1', isIdentified: true); start(status: stored); bloc.add(const SessionEventStarted()); await expectLater(bloc.stream, emits(const SessionStateActive(stored))); gateway.revokeSession(); await expectLater(bloc.stream, emits(const SessionStateSignedOut())); }); test('deleting the account asks how to start', () async { const stored = SignedIn(authUserId: 'github-1', isIdentified: true); start(status: stored); bloc.add(const SessionEventStarted()); await expectLater(bloc.stream, emits(const SessionStateActive(stored))); bloc.add(const SessionEventDeleteRequested()); await expectLater( bloc.stream, emitsInOrder([ const SessionStateActive(stored, busy: SessionAction.delete), const SessionStateSignedOut(), ]), ); expect(gateway.accountDeletions, 1); }); test('a deletion that fails keeps the person signed in, their app in ' 'place, and says so; asked again, it tries again', () async { const stored = SignedIn(authUserId: 'github-1', isIdentified: true); start(status: stored); bloc.add(const SessionEventStarted()); await expectLater(bloc.stream, emits(const SessionStateActive(stored))); gateway.deletionError = Exception('No connection'); bloc.add(const SessionEventDeleteRequested()); await expectLater( bloc.stream, emitsInOrder([ const SessionStateActive(stored, busy: SessionAction.delete), const SessionStateActive(stored, problem: SessionProblem.deleteFailed), ]), ); expect(gateway.status, stored); gateway.deletionError = null; bloc.add(const SessionEventDeleteRequested()); await expectLater( bloc.stream, emitsInOrder([ const SessionStateActive(stored, busy: SessionAction.delete), const SessionStateSignedOut(), ]), ); expect(gateway.accountDeletions, 2); }); test('a sign-out that fails keeps the person signed in, their app in ' 'place, and says so', () async { const stored = SignedIn(authUserId: 'github-1', isIdentified: true); start(status: stored); bloc.add(const SessionEventStarted()); await expectLater(bloc.stream, emits(const SessionStateActive(stored))); gateway.signOutError = Exception('The keyring is locked'); bloc.add(const SessionEventSignOutRequested()); await expectLater( bloc.stream, emitsInOrder([ const SessionStateActive(stored, busy: SessionAction.signOut), const SessionStateActive(stored, problem: SessionProblem.signOutFailed), ]), ); expect(gateway.status, stored); }); }