feat(app)!: no anonymous accounts on the device (ADR 0020, D4, D25)
Every account has a way in, so the apps make no anonymous account any more. The start screen shows whenever nobody is signed in, a first start included, and offers SimpleLogin, a password and GitHub for those who have it, with no start without an account. An instance out of reach is said so, with Try again, and one offering no way to create an account too. An anonymous account's session left from before is deleted as the app starts (D25), falling back to signing out on the device. What only anonymous accounts needed goes: signing in anonymously, the signed-out mark, adding GitHub to an account and switching to a GitHub account from the Profile tab (with its Cancel, the only one), an anonymous device's own keys, the warning that signing out deletes the account, and the texts about keeping progress. Settings says on which instance the account is signed in. Learn's publishing, reports and suggestions and its learner count no longer ask whether the person has an identity: everyone signed in has one. The server still offers anonymous sign-in until the next commit, so older apps keep working until v0.20.0 raises the minimum build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
fad12b4411
commit
5b9df73756
7 changed files with 981 additions and 3339 deletions
|
|
@ -1,315 +0,0 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_app/testing.dart';
|
||||
|
||||
void main() {
|
||||
const anonymous = SignedIn(authUserId: 'anonymous-1', isIdentified: false);
|
||||
|
||||
late FakeAuthGateway gateway;
|
||||
|
||||
/// Whether this device holds content a switch would leave behind.
|
||||
late bool holdsContent;
|
||||
|
||||
setUp(() {
|
||||
gateway = FakeAuthGateway(status: anonymous);
|
||||
holdsContent = false;
|
||||
});
|
||||
|
||||
/// A bloc as the account dialog starts it, once it knows whether this
|
||||
/// instance offers GitHub sign-in.
|
||||
Future<GitHubSignInBloc> ready() async {
|
||||
final bloc = GitHubSignInBloc(
|
||||
AuthRepository(gateway, MemorySignInAddresses()),
|
||||
deviceHoldsContent: () async => holdsContent,
|
||||
)..add(const GitHubSignInEventStarted());
|
||||
addTearDown(bloc.close);
|
||||
await expectLater(bloc.stream, emits(const GitHubSignInStateReady()));
|
||||
return bloc;
|
||||
}
|
||||
|
||||
test('offers nothing where the instance has no GitHub sign-in', () async {
|
||||
gateway.offersGitHub = false;
|
||||
final bloc = GitHubSignInBloc(
|
||||
AuthRepository(gateway, MemorySignInAddresses()),
|
||||
deviceHoldsContent: () async => holdsContent,
|
||||
)..add(const GitHubSignInEventStarted());
|
||||
addTearDown(bloc.close);
|
||||
|
||||
await expectLater(bloc.stream, emits(const GitHubSignInStateUnavailable()));
|
||||
});
|
||||
|
||||
test('offers linking no GitHub account where the instance signs people in '
|
||||
'with a password (ADR 0017)', () async {
|
||||
gateway.linksGitHub = false;
|
||||
final bloc = GitHubSignInBloc(
|
||||
AuthRepository(gateway, MemorySignInAddresses()),
|
||||
deviceHoldsContent: () async => holdsContent,
|
||||
)..add(const GitHubSignInEventStarted());
|
||||
addTearDown(bloc.close);
|
||||
|
||||
await expectLater(
|
||||
bloc.stream,
|
||||
emits(const GitHubSignInStateReady(linking: false)),
|
||||
);
|
||||
});
|
||||
|
||||
test('links the GitHub account to the person', () async {
|
||||
final bloc = await ready();
|
||||
|
||||
bloc.add(const GitHubSignInEventRequested());
|
||||
|
||||
await expectLater(
|
||||
bloc.stream,
|
||||
emitsInOrder([
|
||||
const GitHubSignInStateBusy(),
|
||||
const GitHubSignInStateLinked(),
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
gateway.status,
|
||||
const SignedIn(authUserId: 'anonymous-1', isIdentified: true),
|
||||
);
|
||||
});
|
||||
|
||||
test('changes nothing when the person cancels on GitHub', () async {
|
||||
gateway.gitHubLinkOutcome = GitHubLinkOutcome.cancelled;
|
||||
final bloc = await ready();
|
||||
|
||||
bloc.add(const GitHubSignInEventRequested());
|
||||
|
||||
await expectLater(
|
||||
bloc.stream,
|
||||
emitsInOrder([
|
||||
const GitHubSignInStateBusy(),
|
||||
const GitHubSignInStateReady(),
|
||||
]),
|
||||
);
|
||||
expect(gateway.status, anonymous);
|
||||
});
|
||||
|
||||
test(
|
||||
"the person can cancel while GitHub's page is open; nothing changes",
|
||||
() async {
|
||||
gateway
|
||||
..canCancelGitHub = true
|
||||
..gitHubPageOpens = true;
|
||||
final bloc = await ready();
|
||||
|
||||
bloc.add(const GitHubSignInEventRequested());
|
||||
await expectLater(
|
||||
bloc.stream,
|
||||
emits(const GitHubSignInStateBusy(cancellable: true)),
|
||||
);
|
||||
bloc.add(const GitHubSignInEventCancelled());
|
||||
|
||||
await expectLater(bloc.stream, emits(const GitHubSignInStateReady()));
|
||||
expect(gateway.gitHubCancels, 1);
|
||||
expect(gateway.status, anonymous);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"offers no cancel where the system's sign-in screen has its own",
|
||||
() async {
|
||||
gateway.gitHubPageOpens = true;
|
||||
final bloc = await ready();
|
||||
|
||||
bloc.add(const GitHubSignInEventRequested());
|
||||
|
||||
await expectLater(
|
||||
bloc.stream,
|
||||
emits(
|
||||
isA<GitHubSignInStateBusy>().having(
|
||||
(state) => state.cancellable,
|
||||
'cancellable',
|
||||
isFalse,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"closing the dialog while GitHub's page is open cancels the sign-in",
|
||||
() async {
|
||||
gateway
|
||||
..canCancelGitHub = true
|
||||
..gitHubPageOpens = true;
|
||||
final bloc = await ready();
|
||||
bloc.add(const GitHubSignInEventRequested());
|
||||
await expectLater(
|
||||
bloc.stream,
|
||||
emits(const GitHubSignInStateBusy(cancellable: true)),
|
||||
);
|
||||
|
||||
await bloc.close();
|
||||
|
||||
expect(gateway.gitHubCancels, 1);
|
||||
expect(gateway.status, anonymous);
|
||||
},
|
||||
);
|
||||
|
||||
test('closing the dialog otherwise cancels nothing', () async {
|
||||
final bloc = await ready();
|
||||
|
||||
await bloc.close();
|
||||
|
||||
expect(gateway.gitHubCancels, 0);
|
||||
});
|
||||
|
||||
test("switches right away when the GitHub account is someone else's and "
|
||||
'this device holds nothing', () async {
|
||||
gateway.gitHubLinkOutcome = GitHubLinkOutcome.takenByAnotherPerson;
|
||||
final bloc = await ready();
|
||||
|
||||
bloc.add(const GitHubSignInEventRequested());
|
||||
await expectLater(bloc.stream, emits(const GitHubSignInStateBusy()));
|
||||
await pumpEventQueue();
|
||||
|
||||
expect(gateway.gitHubSwitches, 1);
|
||||
});
|
||||
|
||||
test("someone identified whose GitHub account is another person's is told "
|
||||
'so, never switched: their own account stays theirs', () async {
|
||||
gateway = FakeAuthGateway(
|
||||
status: const SignedIn(authUserId: 'carla', isIdentified: true),
|
||||
)..gitHubLinkOutcome = GitHubLinkOutcome.takenByAnotherPerson;
|
||||
final bloc = await ready();
|
||||
|
||||
bloc.add(const GitHubSignInEventRequested());
|
||||
|
||||
await expectLater(
|
||||
bloc.stream,
|
||||
emitsInOrder([
|
||||
const GitHubSignInStateBusy(),
|
||||
const GitHubSignInStateTaken(),
|
||||
]),
|
||||
);
|
||||
expect(gateway.gitHubSwitches, 0);
|
||||
expect(
|
||||
gateway.status,
|
||||
const SignedIn(authUserId: 'carla', isIdentified: true),
|
||||
);
|
||||
});
|
||||
|
||||
group("when the GitHub account is someone else's and this device holds "
|
||||
'content', () {
|
||||
late GitHubSignInBloc bloc;
|
||||
|
||||
setUp(() async {
|
||||
gateway.gitHubLinkOutcome = GitHubLinkOutcome.takenByAnotherPerson;
|
||||
holdsContent = true;
|
||||
bloc = (await ready())..add(const GitHubSignInEventRequested());
|
||||
await expectLater(
|
||||
bloc.stream,
|
||||
emitsInOrder([
|
||||
const GitHubSignInStateBusy(),
|
||||
const GitHubSignInStateConfirmSwitch(),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('asks first; staying anonymous changes nothing', () async {
|
||||
bloc.add(const GitHubSignInEventSwitchDeclined());
|
||||
|
||||
await expectLater(bloc.stream, emits(const GitHubSignInStateReady()));
|
||||
expect(gateway.gitHubSwitches, 0);
|
||||
expect(gateway.status, anonymous);
|
||||
});
|
||||
|
||||
test('switches once confirmed', () async {
|
||||
bloc.add(const GitHubSignInEventSwitchConfirmed());
|
||||
await expectLater(bloc.stream, emits(const GitHubSignInStateBusy()));
|
||||
await pumpEventQueue();
|
||||
|
||||
expect(gateway.gitHubSwitches, 1);
|
||||
});
|
||||
|
||||
test("cancelling GitHub's page for the switch stays anonymous", () async {
|
||||
gateway
|
||||
..canCancelGitHub = true
|
||||
..gitHubPageOpens = true;
|
||||
|
||||
bloc.add(const GitHubSignInEventSwitchConfirmed());
|
||||
await expectLater(
|
||||
bloc.stream,
|
||||
emits(const GitHubSignInStateBusy(cancellable: true)),
|
||||
);
|
||||
bloc.add(const GitHubSignInEventCancelled());
|
||||
|
||||
await expectLater(bloc.stream, emits(const GitHubSignInStateReady()));
|
||||
expect(gateway.gitHubSwitches, 0);
|
||||
expect(gateway.status, anonymous);
|
||||
});
|
||||
});
|
||||
|
||||
test('says why the instance refused, keeping whether it links', () async {
|
||||
gateway.linksGitHub = false;
|
||||
final bloc = GitHubSignInBloc(
|
||||
AuthRepository(gateway, MemorySignInAddresses()),
|
||||
deviceHoldsContent: () async => holdsContent,
|
||||
)..add(const GitHubSignInEventStarted());
|
||||
addTearDown(bloc.close);
|
||||
await expectLater(
|
||||
bloc.stream,
|
||||
emits(const GitHubSignInStateReady(linking: false)),
|
||||
);
|
||||
gateway.gitHubError = const GitHubSignInFailed(
|
||||
GitHubSignInProblem.deprecated,
|
||||
);
|
||||
|
||||
bloc.add(const GitHubSignInEventRequested());
|
||||
|
||||
await expectLater(
|
||||
bloc.stream,
|
||||
emitsInOrder([
|
||||
const GitHubSignInStateBusy(),
|
||||
const GitHubSignInStateReady(
|
||||
linking: false,
|
||||
failed: true,
|
||||
problem: GitHubSignInProblem.deprecated,
|
||||
),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('an anonymous person whose GitHub account now signs in with its '
|
||||
'password is told so, and stays', () async {
|
||||
gateway
|
||||
..gitHubLinkOutcome = GitHubLinkOutcome.takenByAnotherPerson
|
||||
..gitHubSwitchError = const GitHubSignInFailed(
|
||||
GitHubSignInProblem.usePassword,
|
||||
);
|
||||
final bloc = await ready();
|
||||
|
||||
bloc.add(const GitHubSignInEventRequested());
|
||||
|
||||
await expectLater(
|
||||
bloc.stream,
|
||||
emitsInOrder([
|
||||
const GitHubSignInStateBusy(),
|
||||
const GitHubSignInStateReady(
|
||||
failed: true,
|
||||
problem: GitHubSignInProblem.usePassword,
|
||||
),
|
||||
]),
|
||||
);
|
||||
expect(gateway.status, anonymous);
|
||||
});
|
||||
|
||||
test('says so when signing in fails', () async {
|
||||
final bloc = await ready();
|
||||
gateway.gitHubError = Exception('No connection');
|
||||
|
||||
bloc.add(const GitHubSignInEventRequested());
|
||||
|
||||
await expectLater(
|
||||
bloc.stream,
|
||||
emitsInOrder([
|
||||
const GitHubSignInStateBusy(),
|
||||
const GitHubSignInStateReady(failed: true),
|
||||
]),
|
||||
);
|
||||
expect(gateway.status, anonymous);
|
||||
});
|
||||
}
|
||||
|
|
@ -4,65 +4,80 @@ import 'package:halkyon_app/testing.dart';
|
|||
|
||||
void main() {
|
||||
late FakeAuthGateway gateway;
|
||||
late MemorySignedOutMark mark;
|
||||
late SessionBloc bloc;
|
||||
|
||||
SessionStateActive anonymous(int number) => SessionStateActive(
|
||||
SignedIn(authUserId: 'anonymous-$number', isIdentified: false),
|
||||
);
|
||||
/// 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(), bool signedOut = false}) {
|
||||
void start({AuthStatus status = const SignedOut()}) {
|
||||
gateway = FakeAuthGateway(status: status);
|
||||
mark = MemorySignedOutMark(signedOut);
|
||||
bloc = SessionBloc(AuthRepository(gateway, MemorySignInAddresses()), mark);
|
||||
bloc = SessionBloc(AuthRepository(gateway, MemorySignInAddresses()));
|
||||
}
|
||||
|
||||
tearDown(() => bloc.close());
|
||||
|
||||
test('signs in anonymously at a first start, nobody having signed out '
|
||||
'here', () async {
|
||||
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,
|
||||
emitsInOrder([const SessionStateLoading(), anonymous(1)]),
|
||||
);
|
||||
expect(gateway.signIns, 1);
|
||||
await expectLater(bloc.stream, emits(const SessionStateSignedOut()));
|
||||
expect(gateway.accountDeletions, 0);
|
||||
expect(gateway.signOuts, 0);
|
||||
});
|
||||
|
||||
test('keeps a stored session instead of creating a second account', () async {
|
||||
const stored = SignedIn(authUserId: 'stored', isIdentified: false);
|
||||
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)));
|
||||
expect(gateway.signIns, 0);
|
||||
});
|
||||
|
||||
test('reports a failed sign-in and signs in when asked again', () async {
|
||||
start();
|
||||
gateway.signInError = Exception('No connection');
|
||||
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,
|
||||
emitsInOrder([const SessionStateLoading(), const SessionStateFailed()]),
|
||||
);
|
||||
|
||||
gateway.signInError = null;
|
||||
bloc.add(const SessionEventSignInRequested());
|
||||
await expectLater(
|
||||
bloc.stream,
|
||||
emitsInOrder([const SessionStateLoading(), anonymous(2)]),
|
||||
);
|
||||
await expectLater(bloc.stream, emits(const SessionStateSignedOut()));
|
||||
expect(gateway.signOuts, 1);
|
||||
expect(gateway.status, const SignedOut());
|
||||
});
|
||||
|
||||
test('after signing out, asks how to start, making no account; the next '
|
||||
'start of the app asks again', () async {
|
||||
const stored = SignedIn(authUserId: 'stored', isIdentified: false);
|
||||
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)));
|
||||
|
|
@ -78,36 +93,11 @@ void main() {
|
|||
const SessionStateSignedOut(),
|
||||
]),
|
||||
);
|
||||
// Signing out of an anonymous account deletes it.
|
||||
expect(gateway.accountDeletions, 1);
|
||||
expect(gateway.signIns, 0);
|
||||
expect(mark.signedOut, isTrue);
|
||||
|
||||
await bloc.close();
|
||||
bloc = SessionBloc(AuthRepository(gateway, MemorySignInAddresses()), mark)
|
||||
..add(const SessionEventStarted());
|
||||
await expectLater(bloc.stream, emits(const SessionStateSignedOut()));
|
||||
expect(gateway.signIns, 0);
|
||||
expect(gateway.accountDeletions, 0);
|
||||
});
|
||||
|
||||
test('starting without an account from there signs in anonymously, and '
|
||||
'the device is no longer signed out', () async {
|
||||
start(signedOut: true);
|
||||
bloc.add(const SessionEventStarted());
|
||||
await expectLater(bloc.stream, emits(const SessionStateSignedOut()));
|
||||
|
||||
bloc.add(const SessionEventStartedAnonymously());
|
||||
|
||||
await expectLater(
|
||||
bloc.stream,
|
||||
emitsInOrder([const SessionStateLoading(), anonymous(1)]),
|
||||
);
|
||||
expect(mark.signedOut, isFalse);
|
||||
});
|
||||
|
||||
test('signing in with GitHub from there switches to its person; '
|
||||
'cancelled, it asks again', () async {
|
||||
start(signedOut: true);
|
||||
test('signing in with GitHub from there switches to its person', () async {
|
||||
start();
|
||||
bloc.add(const SessionEventStarted());
|
||||
await expectLater(bloc.stream, emits(const SessionStateSignedOut()));
|
||||
|
||||
|
|
@ -121,13 +111,11 @@ void main() {
|
|||
);
|
||||
bloc.add(const SessionEventPasswordOfferDone());
|
||||
await expectLater(bloc.stream, emits(const SessionStateActive(person)));
|
||||
expect(gateway.signIns, 0);
|
||||
expect(mark.signedOut, isFalse);
|
||||
});
|
||||
|
||||
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(signedOut: true);
|
||||
start();
|
||||
bloc.add(const SessionEventStarted());
|
||||
await expectLater(bloc.stream, emits(const SessionStateSignedOut()));
|
||||
|
||||
|
|
@ -152,13 +140,12 @@ void main() {
|
|||
const SessionStateSignedOut(problem: SignedOutProblem.failed),
|
||||
]),
|
||||
);
|
||||
expect(gateway.signIns, 0);
|
||||
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(signedOut: true);
|
||||
start();
|
||||
bloc.add(const SessionEventStarted());
|
||||
await expectLater(bloc.stream, emits(const SessionStateSignedOut()));
|
||||
|
||||
|
|
@ -183,7 +170,6 @@ void main() {
|
|||
const SessionStateSignedOut(problem: SignedOutProblem.gitHubEnded),
|
||||
]),
|
||||
);
|
||||
expect(gateway.signIns, 0);
|
||||
expect(gateway.gitHubSwitches, 0);
|
||||
});
|
||||
|
||||
|
|
@ -196,7 +182,6 @@ void main() {
|
|||
gateway.revokeSession();
|
||||
|
||||
await expectLater(bloc.stream, emits(const SessionStateSignedOut()));
|
||||
expect(gateway.signIns, 0);
|
||||
});
|
||||
|
||||
test('deleting the account asks how to start', () async {
|
||||
|
|
@ -215,7 +200,6 @@ void main() {
|
|||
]),
|
||||
);
|
||||
expect(gateway.accountDeletions, 1);
|
||||
expect(gateway.signIns, 0);
|
||||
});
|
||||
|
||||
test('a deletion that fails keeps the person signed in, their app in '
|
||||
|
|
@ -236,7 +220,6 @@ void main() {
|
|||
]),
|
||||
);
|
||||
expect(gateway.status, stored);
|
||||
expect(mark.signedOut, isFalse);
|
||||
|
||||
gateway.deletionError = null;
|
||||
bloc.add(const SessionEventDeleteRequested());
|
||||
|
|
@ -269,6 +252,5 @@ void main() {
|
|||
]),
|
||||
);
|
||||
expect(gateway.status, stored);
|
||||
expect(mark.signedOut, isFalse);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,85 +3,79 @@ import 'package:halkyon_app/halkyon_app.dart';
|
|||
import 'package:halkyon_app/testing.dart';
|
||||
|
||||
void main() {
|
||||
group('AuthRepository.ensureSignedIn', () {
|
||||
test('signs in anonymously when nobody is signed in', () async {
|
||||
final gateway = FakeAuthGateway();
|
||||
/// An anonymous account's session, left from before they went (ADR 0020,
|
||||
/// D25).
|
||||
const anonymous = SignedIn(authUserId: 'anonymous-1', isIdentified: false);
|
||||
|
||||
await AuthRepository(gateway, MemorySignInAddresses()).ensureSignedIn();
|
||||
|
||||
expect(gateway.signIns, 1);
|
||||
group('AuthRepository.isAnonymous', () {
|
||||
test('tells a session without an identity, left from before', () {
|
||||
expect(
|
||||
gateway.status,
|
||||
const SignedIn(authUserId: 'anonymous-1', isIdentified: false),
|
||||
AuthRepository(
|
||||
FakeAuthGateway(status: anonymous),
|
||||
MemorySignInAddresses(),
|
||||
).isAnonymous,
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('keeps the person already signed in', () async {
|
||||
const stored = SignedIn(authUserId: 'stored', isIdentified: false);
|
||||
final gateway = FakeAuthGateway(status: stored);
|
||||
|
||||
await AuthRepository(gateway, MemorySignInAddresses()).ensureSignedIn();
|
||||
|
||||
expect(gateway.signIns, 0);
|
||||
expect(gateway.status, stored);
|
||||
test("not an account's, nor nobody signed in", () {
|
||||
expect(
|
||||
AuthRepository(FakeAuthGateway(), MemorySignInAddresses()).isAnonymous,
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
AuthRepository(
|
||||
FakeAuthGateway(status: const SignedOut()),
|
||||
MemorySignInAddresses(),
|
||||
).isAnonymous,
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('AuthRepository.signOut', () {
|
||||
test(
|
||||
'deletes an anonymous account, which nobody could sign into again',
|
||||
() async {
|
||||
final gateway = FakeAuthGateway(
|
||||
status: const SignedIn(
|
||||
authUserId: 'anonymous-1',
|
||||
isIdentified: false,
|
||||
),
|
||||
);
|
||||
group('AuthRepository.endAnonymousSession', () {
|
||||
test('deletes the anonymous account, which nobody could sign into '
|
||||
'again', () async {
|
||||
final gateway = FakeAuthGateway(status: anonymous);
|
||||
|
||||
await AuthRepository(gateway, MemorySignInAddresses()).signOut();
|
||||
await AuthRepository(
|
||||
gateway,
|
||||
MemorySignInAddresses(),
|
||||
).endAnonymousSession();
|
||||
|
||||
expect(gateway.accountDeletions, 1);
|
||||
expect(gateway.signOuts, 0);
|
||||
expect(gateway.status, const SignedOut());
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'gives an anonymous account up all the same when deleting it fails, '
|
||||
'as while the register is out of reach: the device signs out, the '
|
||||
'account left to the pruning (the short reading, L2)',
|
||||
() async {
|
||||
final gateway = FakeAuthGateway(
|
||||
status: const SignedIn(
|
||||
authUserId: 'anonymous-1',
|
||||
isIdentified: false,
|
||||
),
|
||||
)..deletionError = Exception('Internal server error');
|
||||
|
||||
await AuthRepository(gateway, MemorySignInAddresses()).signOut();
|
||||
|
||||
expect(gateway.accountDeletions, 1);
|
||||
expect(gateway.signOuts, 1);
|
||||
expect(gateway.status, const SignedOut());
|
||||
},
|
||||
);
|
||||
|
||||
test('only signs an identified person out on this device', () async {
|
||||
final gateway = FakeAuthGateway(
|
||||
status: const SignedIn(authUserId: 'github-1', isIdentified: true),
|
||||
);
|
||||
|
||||
await AuthRepository(gateway, MemorySignInAddresses()).signOut();
|
||||
|
||||
expect(gateway.signOuts, 1);
|
||||
expect(gateway.accountDeletions, 0);
|
||||
expect(gateway.accountDeletions, 1);
|
||||
expect(gateway.signOuts, 0);
|
||||
expect(gateway.status, const SignedOut());
|
||||
});
|
||||
|
||||
test('gives it up all the same when deleting it fails, as while the '
|
||||
'instance is out of reach: the device signs out, the account left '
|
||||
'to the pruning', () async {
|
||||
final gateway = FakeAuthGateway(status: anonymous)
|
||||
..deletionError = Exception('Internal server error');
|
||||
|
||||
await AuthRepository(
|
||||
gateway,
|
||||
MemorySignInAddresses(),
|
||||
).endAnonymousSession();
|
||||
|
||||
expect(gateway.accountDeletions, 1);
|
||||
expect(gateway.signOuts, 1);
|
||||
expect(gateway.status, const SignedOut());
|
||||
});
|
||||
});
|
||||
|
||||
test('signing out only signs out on this device, deleting nothing', () async {
|
||||
final gateway = FakeAuthGateway();
|
||||
|
||||
await AuthRepository(gateway, MemorySignInAddresses()).signOut();
|
||||
|
||||
expect(gateway.signOuts, 1);
|
||||
expect(gateway.accountDeletions, 0);
|
||||
});
|
||||
|
||||
test('deletes the account of whoever is signed in', () async {
|
||||
final gateway = FakeAuthGateway(
|
||||
status: const SignedIn(authUserId: 'github-1', isIdentified: true),
|
||||
);
|
||||
final gateway = FakeAuthGateway();
|
||||
|
||||
await AuthRepository(gateway, MemorySignInAddresses()).deleteAccount();
|
||||
|
||||
|
|
|
|||
|
|
@ -19,14 +19,12 @@ final class _MemoryStorage() implements ClientAuthSuccessStorage {
|
|||
Future<void> set(AuthSuccess? data) async => _session = data;
|
||||
}
|
||||
|
||||
/// An instance's halkyon_base as the network shows it: anonymous sign-ins
|
||||
/// get `anonymous-token`, its basics offer GitHub (and passwords while
|
||||
/// [passwords], SimpleLogin while [simpleLogin]), and a sign-up makes Ben,
|
||||
/// signed in with `ben-token`.
|
||||
/// An instance's halkyon_base as the network shows it: its basics offer
|
||||
/// GitHub while it has [gitHubClientId], and a sign-up makes Ben, signed
|
||||
/// in with `ben-token`.
|
||||
final class _Instance() {
|
||||
/// Whether its basics offer password sign-in, and SimpleLogin.
|
||||
bool passwords = false;
|
||||
bool simpleLogin = false;
|
||||
/// The client ID of its GitHub OAuth app; none without.
|
||||
String? gitHubClientId = 'github-client';
|
||||
|
||||
static final anonymous = UuidValue.withValidation(
|
||||
'0191f0c4-1111-7000-8000-000000000001',
|
||||
|
|
@ -43,20 +41,16 @@ final class _Instance() {
|
|||
calls.add((call: call, session: request.headers['authorization']));
|
||||
String sent(Object? body) => jsonEncode(body);
|
||||
return switch (call) {
|
||||
'halkyon_base.anonymousIdp/login' => http.Response(
|
||||
sent(_session(anonymous, 'anonymous-token', identified: false)),
|
||||
200,
|
||||
),
|
||||
'halkyon_base.instance/basics' => http.Response(
|
||||
sent(
|
||||
InstanceBasics(
|
||||
app: 'learn',
|
||||
gitHubClientId: 'github-client',
|
||||
gitHubClientId: gitHubClientId,
|
||||
emailSignIn: true,
|
||||
sourceCodeUrl: 'https://code.example.org/learn',
|
||||
minimumAppBuild: 1,
|
||||
passwordSignIn: passwords,
|
||||
simpleLogin: simpleLogin,
|
||||
passwordSignIn: true,
|
||||
simpleLogin: true,
|
||||
).toJson(),
|
||||
),
|
||||
200,
|
||||
|
|
@ -65,7 +59,6 @@ final class _Instance() {
|
|||
sent(_session(ben, 'ben-token', identified: true)),
|
||||
200,
|
||||
),
|
||||
'halkyon_base.account/delete' => http.Response('null', 200),
|
||||
_ => http.Response('', 404),
|
||||
};
|
||||
});
|
||||
|
|
@ -111,46 +104,42 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
test(
|
||||
'signs in anonymously through halkyon_base, nobody identified',
|
||||
() async {
|
||||
await gateway.signInAnonymously();
|
||||
test("tells who is signed in from the session it holds: an account's "
|
||||
"has its identity; one without is an anonymous account's, left from "
|
||||
'before they went (ADR 0020, D25)', () async {
|
||||
expect(gateway.status, const SignedOut());
|
||||
|
||||
expect(
|
||||
gateway.status,
|
||||
SignedIn(authUserId: '${_Instance.anonymous}', isIdentified: false),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('tells what the instance offers from its basics: GitHub sign-in, '
|
||||
'and linking GitHub to people only where neither passwords nor '
|
||||
'SimpleLogin are (ADR 0020, D23)', () async {
|
||||
expect(
|
||||
await gateway.gitHubOffer(),
|
||||
const GitHubOffer(signIn: true, linking: true),
|
||||
);
|
||||
|
||||
instance.passwords = true;
|
||||
expect(
|
||||
await gateway.gitHubOffer(),
|
||||
const GitHubOffer(signIn: true, linking: false),
|
||||
);
|
||||
expect(
|
||||
instance.calls.where(
|
||||
(each) => each.call == 'halkyon_base.instance/basics',
|
||||
await app.auth.updateSignedInUser(
|
||||
AuthSuccess.fromJson(
|
||||
_Instance._session(_Instance.ben, 'ben-token', identified: true),
|
||||
),
|
||||
hasLength(2),
|
||||
reason: 'one read of the basics each',
|
||||
);
|
||||
expect(
|
||||
gateway.status,
|
||||
SignedIn(authUserId: '${_Instance.ben}', isIdentified: true),
|
||||
);
|
||||
|
||||
instance
|
||||
..passwords = false
|
||||
..simpleLogin = true;
|
||||
expect(
|
||||
await gateway.gitHubOffer(),
|
||||
const GitHubOffer(signIn: true, linking: false),
|
||||
await app.auth.updateSignedInUser(
|
||||
AuthSuccess.fromJson(
|
||||
_Instance._session(
|
||||
_Instance.anonymous,
|
||||
'anonymous-token',
|
||||
identified: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
gateway.status,
|
||||
SignedIn(authUserId: '${_Instance.anonymous}', isIdentified: false),
|
||||
);
|
||||
});
|
||||
|
||||
test('tells from the basics whether the instance signs in with GitHub '
|
||||
'those who have it', () async {
|
||||
expect(await gateway.gitHubSignInOffered(), isTrue);
|
||||
|
||||
instance.gitHubClientId = null;
|
||||
expect(await gateway.gitHubSignInOffered(), isFalse);
|
||||
});
|
||||
|
||||
test("puts the instance's refusals of GitHub as the app's problems", () {
|
||||
|
|
@ -172,10 +161,8 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('a session granted elsewhere (a sign-up with a password, without '
|
||||
"this device's session) takes its place, deleting the anonymous "
|
||||
'account left behind as itself', () async {
|
||||
await gateway.signInAnonymously();
|
||||
test('a session granted elsewhere (a sign-up with a password, signed '
|
||||
'out) takes its place', () async {
|
||||
final passwords = ServerpodPasswordGateway(
|
||||
app,
|
||||
base: app.modules,
|
||||
|
|
@ -216,9 +203,11 @@ void main() {
|
|||
(call) => call.call == 'halkyon_base.passwordSignIn/signUp',
|
||||
);
|
||||
expect(signUp.session, isNull, reason: 'no session goes with it');
|
||||
final deletion = instance.calls.singleWhere(
|
||||
(call) => call.call == 'halkyon_base.account/delete',
|
||||
expect(
|
||||
instance.calls.where(
|
||||
(call) => call.call == 'halkyon_base.account/delete',
|
||||
),
|
||||
isEmpty,
|
||||
);
|
||||
expect(deletion.session, contains('anonymous-token'));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,10 +19,6 @@ class const _PersonApp(final SignedIn person) extends StatelessWidget {
|
|||
children: [
|
||||
Text('App of ${person.authUserId}'),
|
||||
const AccountTiles(
|
||||
anonymousNote: 'Your notes stay on this device.',
|
||||
signOutAnonymousMessage:
|
||||
"This account can't be signed into again, so signing out "
|
||||
'deletes it with its notes.',
|
||||
deleteMessage: 'Your notes are deleted everywhere.',
|
||||
),
|
||||
],
|
||||
|
|
@ -64,12 +60,12 @@ void main() {
|
|||
|
||||
setUp(() => gateway = FakeAuthGateway());
|
||||
|
||||
testWidgets('says whose account it is, anonymous on which instance, with '
|
||||
"the app's note", (tester) async {
|
||||
testWidgets('says on which instance the account is signed in', (
|
||||
tester,
|
||||
) async {
|
||||
await start(tester);
|
||||
|
||||
expect(find.text('Anonymous on test.example.org'), findsOneWidget);
|
||||
expect(find.text('Your notes stay on this device.'), findsOneWidget);
|
||||
expect(find.text('Signed in on test.example.org'), findsOneWidget);
|
||||
expect(
|
||||
find.text('Switch account'),
|
||||
findsNothing,
|
||||
|
|
@ -79,18 +75,6 @@ void main() {
|
|||
expect(find.text('Delete account'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('says the account is signed in, without the note', (
|
||||
tester,
|
||||
) async {
|
||||
gateway = FakeAuthGateway(
|
||||
status: const SignedIn(authUserId: 'github-person', isIdentified: true),
|
||||
);
|
||||
await start(tester);
|
||||
|
||||
expect(find.text('Signed in on test.example.org'), findsOneWidget);
|
||||
expect(find.text('Your notes stay on this device.'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('switching account lists the accounts on the device, the '
|
||||
'instance in use named', (tester) async {
|
||||
final instance = Instance.parse('learn.example.org')!;
|
||||
|
|
@ -112,41 +96,7 @@ void main() {
|
|||
expect(find.text('Accounts'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('signing out of an anonymous account asks first, in the '
|
||||
"app's words, as it deletes it; Cancel keeps it", (tester) async {
|
||||
await start(tester);
|
||||
|
||||
await tester.tap(find.text('Sign out'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Sign out?'), findsOneWidget);
|
||||
expect(
|
||||
find.text(
|
||||
"This account can't be signed into again, so signing out deletes "
|
||||
'it with its notes.',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Cancel'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('App of anonymous-1'), findsOneWidget);
|
||||
expect(gateway.signOuts, 0);
|
||||
|
||||
await tester.tap(find.text('Sign out'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Sign out').last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(gateway.status, const SignedOut());
|
||||
expect(find.text('Signed out'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('signing out of an identified account asks nothing', (
|
||||
tester,
|
||||
) async {
|
||||
gateway = FakeAuthGateway(
|
||||
status: const SignedIn(authUserId: 'github-person', isIdentified: true),
|
||||
);
|
||||
testWidgets('signing out asks nothing, deleting nothing', (tester) async {
|
||||
await start(tester);
|
||||
|
||||
await tester.tap(find.text('Sign out'));
|
||||
|
|
@ -154,6 +104,7 @@ void main() {
|
|||
|
||||
expect(find.text('Sign out?'), findsNothing);
|
||||
expect(gateway.status, const SignedOut());
|
||||
expect(gateway.accountDeletions, 0);
|
||||
});
|
||||
|
||||
testWidgets("deleting the account asks first, in the app's words; Cancel "
|
||||
|
|
@ -175,7 +126,7 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
|
||||
expect(gateway.accountDeletions, 1);
|
||||
expect(find.text('Signed out'), findsOneWidget);
|
||||
expect(find.text('Start on test.example.org:'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('deleting an identified account while the instance is out of '
|
||||
|
|
@ -344,7 +295,7 @@ void main() {
|
|||
dependencies.passwordGateway.accounts,
|
||||
isNot(contains('anna@example.org')),
|
||||
);
|
||||
expect(find.text('Signed out'), findsOneWidget);
|
||||
expect(find.text('Start on test.example.org:'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('while the account is deleted, the person stays in their app, '
|
||||
|
|
@ -360,7 +311,7 @@ void main() {
|
|||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(
|
||||
find.text('App of anonymous-1'),
|
||||
find.text('App of person-1'),
|
||||
findsOneWidget,
|
||||
reason: 'never the launch screen in between',
|
||||
);
|
||||
|
|
@ -374,7 +325,7 @@ void main() {
|
|||
|
||||
gateway.deletionWaits!.complete();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Signed out'), findsOneWidget);
|
||||
expect(find.text('Start on test.example.org:'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a deletion the instance never got keeps the person in their '
|
||||
|
|
@ -394,7 +345,7 @@ void main() {
|
|||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('App of anonymous-1'), findsOneWidget);
|
||||
expect(find.text('App of person-1'), findsOneWidget);
|
||||
expect(gateway.status, isA<SignedIn>());
|
||||
expect(find.byType(ButtonLoadingIndicator), findsNothing);
|
||||
});
|
||||
|
|
@ -420,14 +371,9 @@ void main() {
|
|||
await start(tester);
|
||||
final german = lookupHalkyonAppLocalizations(const Locale('de'));
|
||||
|
||||
expect(
|
||||
german.accountAnonymousOn('test.example.org'),
|
||||
'Anonym auf test.example.org',
|
||||
);
|
||||
expect(german.accountSignedInOn('x'), 'Angemeldet auf x');
|
||||
expect(german.accountSwitch, 'Konto wechseln');
|
||||
expect(german.accountSignOut, 'Abmelden');
|
||||
expect(german.accountSignOutTitle, 'Abmelden?');
|
||||
expect(german.accountDelete, 'Konto löschen');
|
||||
expect(german.accountDeleteTitle, 'Dein Konto löschen?');
|
||||
expect(
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_app/l10n/halkyon_app_localizations_en.dart';
|
||||
|
||||
void main() {
|
||||
final en = HalkyonAppLocalizationsEn();
|
||||
|
||||
String? textOf(GitHubSignInProblem? problem, {bool anonymous = false}) =>
|
||||
gitHubProblemText(en, problem, anonymous: anonymous);
|
||||
|
||||
test('each refusal the instance gives a reason for says it', () {
|
||||
expect(
|
||||
textOf(GitHubSignInProblem.unknownAccount),
|
||||
'No account here signs in with this GitHub account yet.',
|
||||
);
|
||||
expect(
|
||||
textOf(GitHubSignInProblem.usePassword),
|
||||
'This account signs in with its email address and password now, not '
|
||||
'with GitHub.',
|
||||
);
|
||||
expect(
|
||||
textOf(GitHubSignInProblem.deprecated),
|
||||
"GitHub can't be added as a way to sign in any more: your email "
|
||||
'address and password are.',
|
||||
);
|
||||
expect(
|
||||
textOf(GitHubSignInProblem.ended),
|
||||
'Sign-in with GitHub has ended on this instance.',
|
||||
);
|
||||
});
|
||||
|
||||
test('for an anonymous person, linking refused means the GitHub account is '
|
||||
"nobody's: a password keeps the account instead", () {
|
||||
expect(
|
||||
textOf(GitHubSignInProblem.deprecated, anonymous: true),
|
||||
'No account here signs in with this GitHub account yet. To keep your '
|
||||
'progress, set up a password instead.',
|
||||
);
|
||||
});
|
||||
|
||||
test("leaves the rest to the screen's own failure", () {
|
||||
expect(textOf(GitHubSignInProblem.refused), isNull);
|
||||
expect(textOf(null), isNull);
|
||||
});
|
||||
}
|
||||
|
|
@ -54,53 +54,61 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets('shows the app once signed in anonymously at a first start', (
|
||||
/// Nobody signed in, as at a first start, on an instance offering every
|
||||
/// way in.
|
||||
void signedOut() {
|
||||
gateway = FakeAuthGateway(status: const SignedOut());
|
||||
dependencies = TestAppDependencies(gateway: gateway);
|
||||
dependencies.simpleLoginGateway.isOffered = true;
|
||||
}
|
||||
|
||||
testWidgets("shows the signed-in person's app", (tester) async {
|
||||
await start(tester);
|
||||
|
||||
expect(find.text('App of person-1'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('nobody signed in, it asks how to start on the instance, '
|
||||
'making no account: there is no starting without one (ADR 0020, D4)', (
|
||||
tester,
|
||||
) async {
|
||||
signedOut();
|
||||
await start(tester);
|
||||
|
||||
expect(find.text('App of anonymous-1'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('offers to try again when signing in fails', (tester) async {
|
||||
gateway.signInError = Exception('No connection');
|
||||
await start(tester);
|
||||
|
||||
expect(find.text("Couldn't reach the server."), findsOneWidget);
|
||||
|
||||
gateway.signInError = null;
|
||||
await tester.tap(find.text('Try again'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('App of anonymous-2'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('signed out, it asks how to start on the instance, making no '
|
||||
'account; starting without one makes one', (tester) async {
|
||||
await dependencies.signedOut.write(signedOut: true);
|
||||
await start(tester);
|
||||
|
||||
expect(find.text('Signed out'), findsOneWidget);
|
||||
expect(find.text('Halkyon Test'), findsOneWidget);
|
||||
expect(find.text('Start on test.example.org:'), findsOneWidget);
|
||||
expect(find.text('Sign in with GitHub'), findsOneWidget);
|
||||
expect(find.text('Continue with SimpleLogin'), findsOneWidget);
|
||||
expect(find.text('Sign in'), findsOneWidget);
|
||||
expect(find.text('Create an account'), findsOneWidget);
|
||||
expect(find.text('Sign in with GitHub'), findsOneWidget);
|
||||
expect(find.text('Start without an account'), findsNothing);
|
||||
expect(
|
||||
find.text('Use another instance'),
|
||||
findsNothing,
|
||||
reason: 'no instances to choose among here',
|
||||
);
|
||||
expect(gateway.signIns, 0);
|
||||
expect(gateway.status, const SignedOut());
|
||||
});
|
||||
|
||||
await tester.tap(find.text('Start without an account'));
|
||||
await tester.pumpAndSettle();
|
||||
testWidgets("an anonymous account's session left from before ends as the "
|
||||
'app starts, the account deleted, and the start screen asks (D25)', (
|
||||
tester,
|
||||
) async {
|
||||
gateway = FakeAuthGateway(
|
||||
status: const SignedIn(authUserId: 'anonymous-1', isIdentified: false),
|
||||
);
|
||||
dependencies = TestAppDependencies(gateway: gateway);
|
||||
await start(tester);
|
||||
|
||||
expect(find.text('App of anonymous-1'), findsOneWidget);
|
||||
expect(dependencies.signedOut.signedOut, isFalse);
|
||||
expect(find.text('App of anonymous-1'), findsNothing);
|
||||
expect(find.text('Start on test.example.org:'), findsOneWidget);
|
||||
expect(gateway.accountDeletions, 1);
|
||||
});
|
||||
|
||||
testWidgets('from there, GitHub signs in its person; a GitHub account '
|
||||
'nobody here has yet is said so, with an account to create in its '
|
||||
'place where passwords are offered (ADR 0017)', (tester) async {
|
||||
await dependencies.signedOut.write(signedOut: true);
|
||||
'nobody here has is said so, with SimpleLogin to create one in its '
|
||||
'place', (tester) async {
|
||||
signedOut();
|
||||
await start(tester);
|
||||
|
||||
gateway.gitHubError = const GitHubSignInFailed(
|
||||
|
|
@ -110,16 +118,11 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.text(
|
||||
'No account here signs in with this GitHub account yet. Create an '
|
||||
'account with your email address and a password instead.',
|
||||
'No account here signs in with this GitHub account. Continue with '
|
||||
'SimpleLogin to create one instead.',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.textContaining('add GitHub'),
|
||||
findsNothing,
|
||||
reason: 'GitHub is added to nobody where passwords are offered',
|
||||
);
|
||||
|
||||
gateway.gitHubError = null;
|
||||
await tester.tap(find.text('Sign in with GitHub'));
|
||||
|
|
@ -130,25 +133,14 @@ void main() {
|
|||
await tester.tap(find.text('Later'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('App of github-person'), findsOneWidget);
|
||||
expect(gateway.signIns, 0);
|
||||
});
|
||||
|
||||
testWidgets('after GitHub, offers no password to someone who has one, or '
|
||||
'where the instance offers none', (tester) async {
|
||||
dependencies.passwordGateway.offered = false;
|
||||
await dependencies.signedOut.write(signedOut: true);
|
||||
await start(tester);
|
||||
|
||||
await tester.tap(find.text('Sign in with GitHub'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Later'), findsNothing);
|
||||
expect(find.text('App of github-person'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('where no passwords are offered, a GitHub account nobody here '
|
||||
'has yet is said so with where the app adds GitHub', (tester) async {
|
||||
dependencies.passwordGateway.offered = false;
|
||||
await dependencies.signedOut.write(signedOut: true);
|
||||
testWidgets('where the instance offers passwords and no SimpleLogin, the '
|
||||
'account to create in its place has a password (ADR 0017)', (
|
||||
tester,
|
||||
) async {
|
||||
signedOut();
|
||||
dependencies.simpleLoginGateway.isOffered = false;
|
||||
await start(tester);
|
||||
|
||||
gateway.gitHubError = const GitHubSignInFailed(
|
||||
|
|
@ -158,17 +150,53 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.text(
|
||||
// The package's sentence, then the app's on where GitHub is added.
|
||||
'No account here signs in with this GitHub account yet. Start '
|
||||
'without an account, then add GitHub in Settings.',
|
||||
'No account here signs in with this GitHub account. Create an '
|
||||
'account with your email address and a password instead.',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('after GitHub, offers no password to someone who has one, or '
|
||||
'where the instance offers none', (tester) async {
|
||||
signedOut();
|
||||
dependencies.passwordGateway.offered = false;
|
||||
await start(tester);
|
||||
|
||||
await tester.tap(find.text('Sign in with GitHub'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Later'), findsNothing);
|
||||
expect(find.text('App of github-person'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('an instance with no way to create an account says so, and '
|
||||
'GitHub signs in only those who have it', (tester) async {
|
||||
signedOut();
|
||||
dependencies.passwordGateway.offered = false;
|
||||
dependencies.simpleLoginGateway.isOffered = false;
|
||||
await start(tester);
|
||||
|
||||
expect(
|
||||
find.text('test.example.org offers no way to create an account.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('Continue with SimpleLogin'), findsNothing);
|
||||
expect(find.text('Create an account'), findsNothing);
|
||||
|
||||
gateway.gitHubError = const GitHubSignInFailed(
|
||||
GitHubSignInProblem.unknownAccount,
|
||||
);
|
||||
await tester.tap(find.text('Sign in with GitHub'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.text('No account here signs in with this GitHub account.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('an account that signs in with its password, and GitHub having '
|
||||
'ended, are each said so', (tester) async {
|
||||
await dependencies.signedOut.write(signedOut: true);
|
||||
signedOut();
|
||||
await start(tester);
|
||||
|
||||
gateway.gitHubError = const GitHubSignInFailed(
|
||||
|
|
@ -195,14 +223,39 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets('offers only the ways the instance has', (tester) async {
|
||||
signedOut();
|
||||
gateway.offersGitHub = false;
|
||||
dependencies.passwordGateway.offered = false;
|
||||
await dependencies.signedOut.write(signedOut: true);
|
||||
await start(tester);
|
||||
|
||||
expect(find.text('Continue with SimpleLogin'), findsOneWidget);
|
||||
expect(find.text('Sign in with GitHub'), findsNothing);
|
||||
expect(find.text('Create an account'), findsNothing);
|
||||
expect(find.text('Start without an account'), findsOneWidget);
|
||||
expect(find.textContaining('offers no way'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('an instance out of reach is said so, offering nothing, and '
|
||||
'asked again', (tester) async {
|
||||
signedOut();
|
||||
dependencies.passwordGateway.failing = const PasswordFailed(
|
||||
PasswordProblem.failed,
|
||||
);
|
||||
dependencies.simpleLoginGateway.failing = const SimpleLoginFailed(
|
||||
SimpleLoginProblem.failed,
|
||||
);
|
||||
gateway.offersGitHubError = Exception('No connection');
|
||||
await start(tester);
|
||||
|
||||
expect(find.text("Couldn't reach the server."), findsOneWidget);
|
||||
expect(find.text('Continue with SimpleLogin'), findsNothing);
|
||||
expect(find.textContaining('offers no way'), findsNothing);
|
||||
|
||||
gateway.offersGitHubError = null;
|
||||
dependencies.passwordGateway.failing = null;
|
||||
dependencies.simpleLoginGateway.failing = null;
|
||||
await tester.tap(find.text('Try again'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Continue with SimpleLogin'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('signing out, or the instance ending the session, asks how to '
|
||||
|
|
@ -216,14 +269,17 @@ void main() {
|
|||
|
||||
await tester.tap(find.text('Sign out'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Signed out'), findsOneWidget);
|
||||
expect(dependencies.signedOut.signedOut, isTrue);
|
||||
expect(find.text('Start on test.example.org:'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Start without an account'));
|
||||
gateway.gitHubError = null;
|
||||
await tester.tap(find.text('Sign in with GitHub'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Later'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('App of github-person'), findsOneWidget);
|
||||
gateway.revokeSession();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Signed out'), findsOneWidget);
|
||||
expect(find.text('Start on test.example.org:'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("a sign-out under way or failed leaves the person's app as it "
|
||||
|
|
|
|||
Loading…
Reference in a new issue