fix(app): GitHub's refusals say why, and no hint adds GitHub where passwords are offered
Where an instance offers password sign-in, GitHub is added to nobody (ADR 0017, D2), yet the start screen still said "then add GitHub" after a GitHub account nobody has, and every refusal but an unknown account read "Couldn't sign in with GitHub." (the end-to-end test of 21 Oct). - The gateway tells from one read of the basics whether GitHub signs in and whether it is added to people (`gitHubOffer`, in place of `gitHubClientId`); the GitHub bloc keeps it (`Ready.linking`) for the profile, with why an attempt failed (`Ready.problem`). - The instance's `usePassword`, `deprecated` and `ended` become problems of their own (`gitHubSignInProblemOf`), worded by `gitHubProblemText` and on the start screen; an anonymous person refused a link is told the GitHub account is nobody's and offered the password instead. - Where passwords are offered, the start screen offers an account to create in place of the app's hint on adding GitHub. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
562d334e6e
commit
19aabb0266
22 changed files with 572 additions and 45 deletions
|
|
@ -15,13 +15,23 @@ void main() {
|
||||||
/// The home tab.
|
/// The home tab.
|
||||||
final home = find.byType(HomeScreen);
|
final home = find.byType(HomeScreen);
|
||||||
|
|
||||||
Future<void> showApp(WidgetTester tester) =>
|
/// Shows the app, on an instance offering passwords unless [passwords] is
|
||||||
pumpApp(tester, testDependencies(authGateway: gateway));
|
/// false.
|
||||||
|
Future<void> showApp(WidgetTester tester, {bool passwords = true}) => pumpApp(
|
||||||
|
tester,
|
||||||
|
testDependencies(
|
||||||
|
authGateway: gateway,
|
||||||
|
passwordGateway: FakePasswordGateway(gateway)..offered = passwords,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
/// Shows the app and opens settings, from the profile tab, down to the
|
/// Shows the app and opens settings, from the profile tab, down to the
|
||||||
/// account.
|
/// account.
|
||||||
Future<void> openSettings(WidgetTester tester) async {
|
Future<void> openSettings(
|
||||||
await showApp(tester);
|
WidgetTester tester, {
|
||||||
|
bool passwords = true,
|
||||||
|
}) async {
|
||||||
|
await showApp(tester, passwords: passwords);
|
||||||
await openTab(tester, 'Profile');
|
await openTab(tester, 'Profile');
|
||||||
await tester.tap(find.byTooltip('Settings'));
|
await tester.tap(find.byTooltip('Settings'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
@ -156,7 +166,8 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('from there, a GitHub account nobody here signs in with is said '
|
testWidgets('from there, a GitHub account nobody here signs in with is said '
|
||||||
'so, with where Learn adds GitHub', (tester) async {
|
'so, with an account to create in its place where passwords are '
|
||||||
|
'offered (ADR 0017)', (tester) async {
|
||||||
await openSettings(tester);
|
await openSettings(tester);
|
||||||
await tester.tap(find.text('Sign out'));
|
await tester.tap(find.text('Sign out'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
@ -169,6 +180,31 @@ void main() {
|
||||||
await tester.tap(find.text('Sign in with GitHub'));
|
await tester.tap(find.text('Sign in with GitHub'));
|
||||||
await tester.pumpAndSettle();
|
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.',
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(gateway.status, const SignedOut());
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('where no passwords are offered, with where Learn adds GitHub', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await openSettings(tester, passwords: false);
|
||||||
|
await tester.tap(find.text('Sign out'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('Sign out').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
gateway.gitHubError = const GitHubSignInFailed(
|
||||||
|
GitHubSignInProblem.unknownAccount,
|
||||||
|
);
|
||||||
|
await tester.tap(find.text('Sign in with GitHub'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
find.text(
|
find.text(
|
||||||
'No account here signs in with this GitHub account yet. Start '
|
'No account here signs in with this GitHub account yet. Start '
|
||||||
|
|
@ -176,7 +212,6 @@ void main() {
|
||||||
),
|
),
|
||||||
findsOneWidget,
|
findsOneWidget,
|
||||||
);
|
);
|
||||||
expect(gateway.status, const SignedOut());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('deleting the account asks first; Cancel keeps it', (
|
testWidgets('deleting the account asks first; Cancel keeps it', (
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ void main() {
|
||||||
WidgetTester tester, {
|
WidgetTester tester, {
|
||||||
bool usedBefore = false,
|
bool usedBefore = false,
|
||||||
bool wasSignedOut = false,
|
bool wasSignedOut = false,
|
||||||
|
bool passwords = true,
|
||||||
}) async {
|
}) async {
|
||||||
tester.view.physicalSize = const Size(412, 900);
|
tester.view.physicalSize = const Size(412, 900);
|
||||||
tester.view.devicePixelRatio = 1;
|
tester.view.devicePixelRatio = 1;
|
||||||
|
|
@ -50,7 +51,7 @@ void main() {
|
||||||
auth: AuthRepository(gateway, addresses),
|
auth: AuthRepository(gateway, addresses),
|
||||||
signedOut: signedOut,
|
signedOut: signedOut,
|
||||||
passwords: PasswordRepository(
|
passwords: PasswordRepository(
|
||||||
FakePasswordGateway(gateway),
|
FakePasswordGateway(gateway)..offered = passwords,
|
||||||
FakeAccountCrypto(),
|
FakeAccountCrypto(),
|
||||||
MemoryAccountKeyStore(),
|
MemoryAccountKeyStore(),
|
||||||
gateway,
|
gateway,
|
||||||
|
|
@ -113,7 +114,8 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('signed out, it asks how to start; a GitHub account nobody here '
|
testWidgets('signed out, it asks how to start; a GitHub account nobody here '
|
||||||
"signs in with is said so, with Todo's ways in", (tester) async {
|
'signs in with is said so, with an account to create in its place '
|
||||||
|
'where passwords are offered (ADR 0017)', (tester) async {
|
||||||
await start(tester, usedBefore: true, wasSignedOut: true);
|
await start(tester, usedBefore: true, wasSignedOut: true);
|
||||||
|
|
||||||
expect(find.text('Sign in with GitHub'), findsOneWidget);
|
expect(find.text('Sign in with GitHub'), findsOneWidget);
|
||||||
|
|
@ -123,6 +125,26 @@ void main() {
|
||||||
await tester.tap(find.text('Sign in with GitHub'));
|
await tester.tap(find.text('Sign in with GitHub'));
|
||||||
await tester.pumpAndSettle();
|
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.',
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets("where no passwords are offered, with Todo's ways in", (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await start(tester, usedBefore: true, wasSignedOut: true, passwords: false);
|
||||||
|
|
||||||
|
gateway.gitHubError = const GitHubSignInFailed(
|
||||||
|
GitHubSignInProblem.unknownAccount,
|
||||||
|
);
|
||||||
|
await tester.tap(find.text('Sign in with GitHub'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
find.text(
|
find.text(
|
||||||
'No account here signs in with this GitHub account yet. Start '
|
'No account here signs in with this GitHub account yet. Start '
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ export 'src/auth/model/github_sign_in.dart';
|
||||||
export 'src/auth/model/session_grant.dart';
|
export 'src/auth/model/session_grant.dart';
|
||||||
export 'src/auth/model/sign_in_ways.dart';
|
export 'src/auth/model/sign_in_ways.dart';
|
||||||
export 'src/auth/widget/account_tiles.dart';
|
export 'src/auth/widget/account_tiles.dart';
|
||||||
|
export 'src/auth/widget/github_texts.dart';
|
||||||
export 'src/auth/widget/session_gate.dart';
|
export 'src/auth/widget/session_gate.dart';
|
||||||
export 'src/auth/widget/start_screen.dart';
|
export 'src/auth/widget/start_screen.dart';
|
||||||
export 'src/base/base_client.dart';
|
export 'src/base/base_client.dart';
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,11 @@
|
||||||
"startAnonymous": "Dein Fortschritt wird auf diesem Gerät gespeichert. Melde dich an, um ihn zu behalten.",
|
"startAnonymous": "Dein Fortschritt wird auf diesem Gerät gespeichert. Melde dich an, um ihn zu behalten.",
|
||||||
"startGitHubFailed": "Anmeldung mit GitHub fehlgeschlagen.",
|
"startGitHubFailed": "Anmeldung mit GitHub fehlgeschlagen.",
|
||||||
"startNoGitHubAccount": "Noch kein Konto hier meldet sich mit diesem GitHub-Konto an.",
|
"startNoGitHubAccount": "Noch kein Konto hier meldet sich mit diesem GitHub-Konto an.",
|
||||||
|
"gitHubCreateAccountInstead": "Erstelle stattdessen ein Konto mit deiner E-Mail-Adresse und einem Passwort.",
|
||||||
|
"gitHubNoAccountSetUpPassword": "Noch kein Konto hier meldet sich mit diesem GitHub-Konto an. Richte stattdessen ein Passwort ein, um deinen Fortschritt zu behalten.",
|
||||||
|
"gitHubUsePassword": "Dieses Konto meldet sich jetzt mit seiner E-Mail-Adresse und seinem Passwort an, nicht mehr mit GitHub.",
|
||||||
|
"gitHubDeprecated": "GitHub lässt sich nicht mehr als Anmeldeweg hinzufügen: Deine E-Mail-Adresse und dein Passwort sind es.",
|
||||||
|
"gitHubEnded": "Die Anmeldung mit GitHub ist auf dieser Instanz beendet.",
|
||||||
"startOtherInstance": "Andere Instanz verwenden",
|
"startOtherInstance": "Andere Instanz verwenden",
|
||||||
"syncTitle": "Synchronisierung",
|
"syncTitle": "Synchronisierung",
|
||||||
"syncNow": "Jetzt synchronisieren",
|
"syncNow": "Jetzt synchronisieren",
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,17 @@
|
||||||
"startAnonymous": "Progress is saved on this device. Sign in to keep it.",
|
"startAnonymous": "Progress is saved on this device. Sign in to keep it.",
|
||||||
"startGitHubFailed": "Couldn't sign in with GitHub.",
|
"startGitHubFailed": "Couldn't sign in with GitHub.",
|
||||||
"startNoGitHubAccount": "No account here signs in with this GitHub account yet.",
|
"startNoGitHubAccount": "No account here signs in with this GitHub account yet.",
|
||||||
|
"gitHubCreateAccountInstead": "Create an account with your email address and a password instead.",
|
||||||
|
"@gitHubCreateAccountInstead": {
|
||||||
|
"description": "Said after a GitHub account nobody on the instance signs in with, where people sign in with a password and GitHub is added to nobody (ADR 0017)."
|
||||||
|
},
|
||||||
|
"gitHubNoAccountSetUpPassword": "No account here signs in with this GitHub account yet. To keep your progress, set up a password instead.",
|
||||||
|
"@gitHubNoAccountSetUpPassword": {
|
||||||
|
"description": "An anonymous person signed in with a GitHub account nobody here has, where GitHub is added to nobody: the password set up below keeps their account."
|
||||||
|
},
|
||||||
|
"gitHubUsePassword": "This account signs in with its email address and password now, not with GitHub.",
|
||||||
|
"gitHubDeprecated": "GitHub can't be added as a way to sign in any more: your email address and password are.",
|
||||||
|
"gitHubEnded": "Sign-in with GitHub has ended on this instance.",
|
||||||
"startOtherInstance": "Use another instance",
|
"startOtherInstance": "Use another instance",
|
||||||
"syncTitle": "Sync",
|
"syncTitle": "Sync",
|
||||||
"syncNow": "Sync now",
|
"syncNow": "Sync now",
|
||||||
|
|
|
||||||
|
|
@ -403,6 +403,36 @@ abstract class HalkyonAppLocalizations {
|
||||||
/// **'No account here signs in with this GitHub account yet.'**
|
/// **'No account here signs in with this GitHub account yet.'**
|
||||||
String get startNoGitHubAccount;
|
String get startNoGitHubAccount;
|
||||||
|
|
||||||
|
/// Said after a GitHub account nobody on the instance signs in with, where people sign in with a password and GitHub is added to nobody (ADR 0017).
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Create an account with your email address and a password instead.'**
|
||||||
|
String get gitHubCreateAccountInstead;
|
||||||
|
|
||||||
|
/// An anonymous person signed in with a GitHub account nobody here has, where GitHub is added to nobody: the password set up below keeps their account.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No account here signs in with this GitHub account yet. To keep your progress, set up a password instead.'**
|
||||||
|
String get gitHubNoAccountSetUpPassword;
|
||||||
|
|
||||||
|
/// No description provided for @gitHubUsePassword.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'This account signs in with its email address and password now, not with GitHub.'**
|
||||||
|
String get gitHubUsePassword;
|
||||||
|
|
||||||
|
/// No description provided for @gitHubDeprecated.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'GitHub can\'t be added as a way to sign in any more: your email address and password are.'**
|
||||||
|
String get gitHubDeprecated;
|
||||||
|
|
||||||
|
/// No description provided for @gitHubEnded.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Sign-in with GitHub has ended on this instance.'**
|
||||||
|
String get gitHubEnded;
|
||||||
|
|
||||||
/// No description provided for @startOtherInstance.
|
/// No description provided for @startOtherInstance.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,26 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
||||||
String get startNoGitHubAccount =>
|
String get startNoGitHubAccount =>
|
||||||
'Noch kein Konto hier meldet sich mit diesem GitHub-Konto an.';
|
'Noch kein Konto hier meldet sich mit diesem GitHub-Konto an.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get gitHubCreateAccountInstead =>
|
||||||
|
'Erstelle stattdessen ein Konto mit deiner E-Mail-Adresse und einem Passwort.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get gitHubNoAccountSetUpPassword =>
|
||||||
|
'Noch kein Konto hier meldet sich mit diesem GitHub-Konto an. Richte stattdessen ein Passwort ein, um deinen Fortschritt zu behalten.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get gitHubUsePassword =>
|
||||||
|
'Dieses Konto meldet sich jetzt mit seiner E-Mail-Adresse und seinem Passwort an, nicht mehr mit GitHub.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get gitHubDeprecated =>
|
||||||
|
'GitHub lässt sich nicht mehr als Anmeldeweg hinzufügen: Deine E-Mail-Adresse und dein Passwort sind es.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get gitHubEnded =>
|
||||||
|
'Die Anmeldung mit GitHub ist auf dieser Instanz beendet.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get startOtherInstance => 'Andere Instanz verwenden';
|
String get startOtherInstance => 'Andere Instanz verwenden';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,25 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
||||||
String get startNoGitHubAccount =>
|
String get startNoGitHubAccount =>
|
||||||
'No account here signs in with this GitHub account yet.';
|
'No account here signs in with this GitHub account yet.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get gitHubCreateAccountInstead =>
|
||||||
|
'Create an account with your email address and a password instead.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get gitHubNoAccountSetUpPassword =>
|
||||||
|
'No account here signs in with this GitHub account yet. To keep your progress, set up a password instead.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get gitHubUsePassword =>
|
||||||
|
'This account signs in with its email address and password now, not with GitHub.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get gitHubDeprecated =>
|
||||||
|
'GitHub can\'t be added as a way to sign in any more: your email address and password are.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get gitHubEnded => 'Sign-in with GitHub has ended on this instance.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get startOtherInstance => 'Use another instance';
|
String get startOtherInstance => 'Use another instance';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:halkyon_app/src/auth/data/auth_repository.dart';
|
import 'package:halkyon_app/src/auth/data/auth_repository.dart';
|
||||||
import 'package:halkyon_app/src/auth/model/github_link_outcome.dart';
|
import 'package:halkyon_app/src/auth/model/github_link_outcome.dart';
|
||||||
|
import 'package:halkyon_app/src/auth/model/github_sign_in.dart';
|
||||||
|
|
||||||
/// Signs an anonymous person in with GitHub, keeping everything they have,
|
/// Signs an anonymous person in with GitHub, keeping everything they have,
|
||||||
/// or adds GitHub as a way in for someone who signs in with an email
|
/// or adds GitHub as a way in for someone who signs in with an email
|
||||||
|
|
@ -13,6 +14,11 @@ import 'package:halkyon_app/src/auth/model/github_link_outcome.dart';
|
||||||
/// the person confirms that this device's content stays behind. Someone
|
/// the person confirms that this device's content stays behind. Someone
|
||||||
/// identified is never switched, as their own account would be left: they
|
/// identified is never switched, as their own account would be left: they
|
||||||
/// are told the GitHub account is another's.
|
/// are told the GitHub account is another's.
|
||||||
|
///
|
||||||
|
/// Where people sign in with a password, GitHub is added to nobody (ADR
|
||||||
|
/// 0017, D2): [GitHubSignInStateReady.linking] says so, and an anonymous
|
||||||
|
/// person reaches only the GitHub account someone has already, by the same
|
||||||
|
/// switch.
|
||||||
class GitHubSignInBloc(
|
class GitHubSignInBloc(
|
||||||
final AuthRepository _auth, {
|
final AuthRepository _auth, {
|
||||||
|
|
||||||
|
|
@ -26,7 +32,7 @@ class GitHubSignInBloc(
|
||||||
on<GitHubSignInEventRequested>(_onRequested);
|
on<GitHubSignInEventRequested>(_onRequested);
|
||||||
on<GitHubSignInEventSwitchConfirmed>(_onSwitchConfirmed);
|
on<GitHubSignInEventSwitchConfirmed>(_onSwitchConfirmed);
|
||||||
on<GitHubSignInEventSwitchDeclined>(
|
on<GitHubSignInEventSwitchDeclined>(
|
||||||
(event, emit) => emit(const GitHubSignInStateReady()),
|
(event, emit) => emit(GitHubSignInStateReady(linking: _linking)),
|
||||||
);
|
);
|
||||||
// The sign-in in progress then ends as cancelled.
|
// The sign-in in progress then ends as cancelled.
|
||||||
on<GitHubSignInEventCancelled>((event, emit) => _auth.cancelGitHub());
|
on<GitHubSignInEventCancelled>((event, emit) => _auth.cancelGitHub());
|
||||||
|
|
@ -43,14 +49,20 @@ class GitHubSignInBloc(
|
||||||
await super.close();
|
await super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether this instance adds GitHub to people as a way in, as it said
|
||||||
|
/// when the dialog started.
|
||||||
|
bool _linking = true;
|
||||||
|
|
||||||
Future<void> _onStarted(
|
Future<void> _onStarted(
|
||||||
GitHubSignInEventStarted event,
|
GitHubSignInEventStarted event,
|
||||||
Emitter<GitHubSignInState> emit,
|
Emitter<GitHubSignInState> emit,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
|
final offer = await _auth.gitHubOffer();
|
||||||
|
_linking = offer.linking;
|
||||||
emit(
|
emit(
|
||||||
await _auth.canSignInWithGitHub()
|
offer.signIn
|
||||||
? const GitHubSignInStateReady()
|
? GitHubSignInStateReady(linking: _linking)
|
||||||
: const GitHubSignInStateUnavailable(),
|
: const GitHubSignInStateUnavailable(),
|
||||||
);
|
);
|
||||||
} on Exception catch (error, st) {
|
} on Exception catch (error, st) {
|
||||||
|
|
@ -70,7 +82,7 @@ class GitHubSignInBloc(
|
||||||
case GitHubLinkOutcome.linked:
|
case GitHubLinkOutcome.linked:
|
||||||
emit(const GitHubSignInStateLinked());
|
emit(const GitHubSignInStateLinked());
|
||||||
case GitHubLinkOutcome.cancelled:
|
case GitHubLinkOutcome.cancelled:
|
||||||
emit(const GitHubSignInStateReady());
|
emit(GitHubSignInStateReady(linking: _linking));
|
||||||
case GitHubLinkOutcome.takenByAnotherPerson when _auth.isIdentified:
|
case GitHubLinkOutcome.takenByAnotherPerson when _auth.isIdentified:
|
||||||
emit(const GitHubSignInStateTaken());
|
emit(const GitHubSignInStateTaken());
|
||||||
case GitHubLinkOutcome.takenByAnotherPerson:
|
case GitHubLinkOutcome.takenByAnotherPerson:
|
||||||
|
|
@ -81,8 +93,7 @@ class GitHubSignInBloc(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} on Exception catch (error, st) {
|
} on Exception catch (error, st) {
|
||||||
addError(error, st);
|
_failed(emit, error, st);
|
||||||
emit(const GitHubSignInStateReady(failed: true));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,16 +105,34 @@ class GitHubSignInBloc(
|
||||||
try {
|
try {
|
||||||
await _switch(emit);
|
await _switch(emit);
|
||||||
} on Exception catch (error, st) {
|
} on Exception catch (error, st) {
|
||||||
addError(error, st);
|
_failed(emit, error, st);
|
||||||
emit(const GitHubSignInStateReady(failed: true));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ready again after [error]: why, where the instance said so.
|
||||||
|
void _failed(
|
||||||
|
Emitter<GitHubSignInState> emit,
|
||||||
|
Exception error,
|
||||||
|
StackTrace st,
|
||||||
|
) {
|
||||||
|
addError(error, st);
|
||||||
|
emit(
|
||||||
|
GitHubSignInStateReady(
|
||||||
|
linking: _linking,
|
||||||
|
failed: true,
|
||||||
|
problem: switch (error) {
|
||||||
|
GitHubSignInFailed(:final problem) => problem,
|
||||||
|
_ => null,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// After a switch, the previous person's whole subtree goes away, this bloc
|
// After a switch, the previous person's whole subtree goes away, this bloc
|
||||||
// with it, so nothing is emitted then.
|
// with it, so nothing is emitted then.
|
||||||
Future<void> _switch(Emitter<GitHubSignInState> emit) async {
|
Future<void> _switch(Emitter<GitHubSignInState> emit) async {
|
||||||
if (!await _auth.switchToGitHub()) {
|
if (!await _auth.switchToGitHub()) {
|
||||||
emit(const GitHubSignInStateReady());
|
emit(GitHubSignInStateReady(linking: _linking));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -144,11 +173,19 @@ final class const GitHubSignInStateUnavailable() extends GitHubSignInState;
|
||||||
|
|
||||||
/// Ready to sign in with GitHub.
|
/// Ready to sign in with GitHub.
|
||||||
final class const GitHubSignInStateReady({
|
final class const GitHubSignInStateReady({
|
||||||
|
/// Whether GitHub is added to people as a way in here: not where they
|
||||||
|
/// sign in with a password (ADR 0017, D2), where someone identified is
|
||||||
|
/// offered no Add.
|
||||||
|
final bool linking = true,
|
||||||
|
|
||||||
/// Whether the last attempt failed.
|
/// Whether the last attempt failed.
|
||||||
final bool failed = false,
|
final bool failed = false,
|
||||||
|
|
||||||
|
/// Why it failed, where the instance said; null otherwise.
|
||||||
|
final GitHubSignInProblem? problem,
|
||||||
}) extends GitHubSignInState {
|
}) extends GitHubSignInState {
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [failed];
|
List<Object?> get props => [linking, failed, problem];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Signing in: GitHub's page is open, or the server is being asked.
|
/// Signing in: GitHub's page is open, or the server is being asked.
|
||||||
|
|
|
||||||
|
|
@ -102,9 +102,14 @@ class SessionBloc(final AuthRepository _auth, final SignedOutMark _mark)
|
||||||
_offerPassword = false;
|
_offerPassword = false;
|
||||||
emit(
|
emit(
|
||||||
SessionStateSignedOut(
|
SessionStateSignedOut(
|
||||||
problem: e.problem == GitHubSignInProblem.unknownAccount
|
problem: switch (e.problem) {
|
||||||
? SignedOutProblem.noGitHubAccount
|
GitHubSignInProblem.unknownAccount =>
|
||||||
: SignedOutProblem.failed,
|
SignedOutProblem.noGitHubAccount,
|
||||||
|
GitHubSignInProblem.usePassword => SignedOutProblem.usePassword,
|
||||||
|
GitHubSignInProblem.ended => SignedOutProblem.gitHubEnded,
|
||||||
|
GitHubSignInProblem.deprecated ||
|
||||||
|
GitHubSignInProblem.refused => SignedOutProblem.failed,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} on Object catch (error, st) {
|
} on Object catch (error, st) {
|
||||||
|
|
@ -246,9 +251,16 @@ final class const SessionStateSignedOut({
|
||||||
/// Why a way to start failed.
|
/// Why a way to start failed.
|
||||||
enum SignedOutProblem() {
|
enum SignedOutProblem() {
|
||||||
/// No account has the GitHub account yet: start without one, then add
|
/// No account has the GitHub account yet: start without one, then add
|
||||||
/// GitHub.
|
/// GitHub, or, where passwords are offered, create an account.
|
||||||
noGitHubAccount,
|
noGitHubAccount,
|
||||||
|
|
||||||
|
/// The GitHub account's person signs in with their address and password
|
||||||
|
/// now (ADR 0017).
|
||||||
|
usePassword,
|
||||||
|
|
||||||
|
/// GitHub signs nobody in here any more.
|
||||||
|
gitHubEnded,
|
||||||
|
|
||||||
/// Anything else, as no connection.
|
/// Anything else, as no connection.
|
||||||
failed,
|
failed,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:halkyon_app/src/auth/model/auth_status.dart';
|
import 'package:halkyon_app/src/auth/model/auth_status.dart';
|
||||||
import 'package:halkyon_app/src/auth/model/github_link_outcome.dart';
|
import 'package:halkyon_app/src/auth/model/github_link_outcome.dart';
|
||||||
|
import 'package:halkyon_app/src/auth/model/github_sign_in.dart';
|
||||||
import 'package:halkyon_app/src/auth/model/session_grant.dart';
|
import 'package:halkyon_app/src/auth/model/session_grant.dart';
|
||||||
import 'package:halkyon_app/src/auth/model/sign_in_ways.dart';
|
import 'package:halkyon_app/src/auth/model/sign_in_ways.dart';
|
||||||
|
|
||||||
|
|
@ -22,9 +23,9 @@ abstract interface class AuthGateway() {
|
||||||
/// and so on all their devices, and forgets the session here.
|
/// and so on all their devices, and forgets the session here.
|
||||||
Future<void> deleteAccount();
|
Future<void> deleteAccount();
|
||||||
|
|
||||||
/// The client ID of this instance's GitHub OAuth app; null when GitHub
|
/// What this instance offers of GitHub: signing in, and adding it to
|
||||||
/// sign-in is off on this instance.
|
/// people as a way in.
|
||||||
Future<String?> gitHubClientId();
|
Future<GitHubOffer> gitHubOffer();
|
||||||
|
|
||||||
/// Authorizes with GitHub in the browser and links the GitHub account to the
|
/// Authorizes with GitHub in the browser and links the GitHub account to the
|
||||||
/// signed-in person: the lossless upgrade from an anonymous account.
|
/// signed-in person: the lossless upgrade from an anonymous account.
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:halkyon_app/src/auth/data/auth_gateway.dart';
|
||||||
import 'package:halkyon_app/src/auth/data/sign_in_addresses.dart';
|
import 'package:halkyon_app/src/auth/data/sign_in_addresses.dart';
|
||||||
import 'package:halkyon_app/src/auth/model/auth_status.dart';
|
import 'package:halkyon_app/src/auth/model/auth_status.dart';
|
||||||
import 'package:halkyon_app/src/auth/model/github_link_outcome.dart';
|
import 'package:halkyon_app/src/auth/model/github_link_outcome.dart';
|
||||||
|
import 'package:halkyon_app/src/auth/model/github_sign_in.dart';
|
||||||
import 'package:halkyon_app/src/auth/model/sign_in_ways.dart';
|
import 'package:halkyon_app/src/auth/model/sign_in_ways.dart';
|
||||||
|
|
||||||
/// Who is signed in, and signing in and out, as the features use it.
|
/// Who is signed in, and signing in and out, as the features use it.
|
||||||
|
|
@ -61,7 +62,11 @@ class AuthRepository(
|
||||||
|
|
||||||
/// Whether this instance offers signing in with GitHub.
|
/// Whether this instance offers signing in with GitHub.
|
||||||
Future<bool> canSignInWithGitHub() async =>
|
Future<bool> canSignInWithGitHub() async =>
|
||||||
await _gateway.gitHubClientId() != null;
|
(await _gateway.gitHubOffer()).signIn;
|
||||||
|
|
||||||
|
/// What this instance offers of GitHub: signing in, and adding it to
|
||||||
|
/// people as a way in (not where they sign in with a password).
|
||||||
|
Future<GitHubOffer> gitHubOffer() => _gateway.gitHubOffer();
|
||||||
|
|
||||||
/// Links a GitHub account to the signed-in person; the person, and all
|
/// Links a GitHub account to the signed-in person; the person, and all
|
||||||
/// their data, stay the same.
|
/// their data, stay the same.
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,18 @@ class ServerpodAuthGateway(
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> gitHubClientId() async =>
|
Future<GitHubOffer> gitHubOffer() async {
|
||||||
|
final basics = await _base.instance.basics();
|
||||||
|
final signIn = basics.gitHubClientId != null;
|
||||||
|
return GitHubOffer(
|
||||||
|
signIn: signIn,
|
||||||
|
linking: signIn && !basics.passwordSignIn,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The client ID of this instance's GitHub OAuth app; null when GitHub
|
||||||
|
/// sign-in is off on this instance.
|
||||||
|
Future<String?> _gitHubClientId() async =>
|
||||||
(await _base.instance.basics()).gitHubClientId;
|
(await _base.instance.basics()).gitHubClientId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -197,7 +208,7 @@ class ServerpodAuthGateway(
|
||||||
/// cancelled it.
|
/// cancelled it.
|
||||||
Future<GitHubSignInResult?> _authorizeWithGitHub() async {
|
Future<GitHubSignInResult?> _authorizeWithGitHub() async {
|
||||||
_gitHubCancelled = false;
|
_gitHubCancelled = false;
|
||||||
final clientId = await gitHubClientId();
|
final clientId = await _gitHubClientId();
|
||||||
if (clientId == null) {
|
if (clientId == null) {
|
||||||
throw StateError('This instance offers no GitHub sign-in.');
|
throw StateError('This instance offers no GitHub sign-in.');
|
||||||
}
|
}
|
||||||
|
|
@ -247,11 +258,7 @@ class ServerpodAuthGateway(
|
||||||
|
|
||||||
/// The instance's refusal of a GitHub sign-in as [GitHubSignInFailed].
|
/// The instance's refusal of a GitHub sign-in as [GitHubSignInFailed].
|
||||||
static GitHubSignInFailed _gitHubFailure(GitHubSignInRefusedException e) =>
|
static GitHubSignInFailed _gitHubFailure(GitHubSignInRefusedException e) =>
|
||||||
GitHubSignInFailed(
|
GitHubSignInFailed(gitHubSignInProblemOf(e.reason));
|
||||||
e.reason == GitHubSignInRefusal.unknownAccount
|
|
||||||
? GitHubSignInProblem.unknownAccount
|
|
||||||
: GitHubSignInProblem.refused,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Deletes the anonymous account this device leaves behind when switching:
|
/// Deletes the anonymous account this device leaves behind when switching:
|
||||||
/// nobody could ever sign into it again. It signs in as that account from
|
/// nobody could ever sign into it again. It signs in as that account from
|
||||||
|
|
@ -301,3 +308,17 @@ final class _MemoryAuthStorage() implements ClientAuthSuccessStorage {
|
||||||
_session = data;
|
_session = data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The instance's refusal [reason] of a GitHub sign-in as the app's problem,
|
||||||
|
/// which the screens put into words.
|
||||||
|
GitHubSignInProblem gitHubSignInProblemOf(GitHubSignInRefusal reason) =>
|
||||||
|
switch (reason) {
|
||||||
|
GitHubSignInRefusal.unknownAccount => GitHubSignInProblem.unknownAccount,
|
||||||
|
GitHubSignInRefusal.usePassword => GitHubSignInProblem.usePassword,
|
||||||
|
GitHubSignInRefusal.deprecated => GitHubSignInProblem.deprecated,
|
||||||
|
GitHubSignInRefusal.ended => GitHubSignInProblem.ended,
|
||||||
|
GitHubSignInRefusal.unavailable ||
|
||||||
|
GitHubSignInRefusal.takenByAnotherPerson ||
|
||||||
|
GitHubSignInRefusal.otherGitHubAccountLinked =>
|
||||||
|
GitHubSignInProblem.refused,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,22 @@
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
/// Why signing in with GitHub didn't go through, beyond a cancel.
|
/// Why signing in with GitHub didn't go through, beyond a cancel.
|
||||||
enum GitHubSignInProblem() {
|
enum GitHubSignInProblem() {
|
||||||
/// No person has this GitHub account yet; only adding it to one makes it
|
/// No person has this GitHub account yet; only adding it to one makes it
|
||||||
/// a way in.
|
/// a way in.
|
||||||
unknownAccount,
|
unknownAccount,
|
||||||
|
|
||||||
|
/// The account signs in with its address and password now, not with
|
||||||
|
/// GitHub (ADR 0017).
|
||||||
|
usePassword,
|
||||||
|
|
||||||
|
/// GitHub is added to nobody where people sign in with a password
|
||||||
|
/// (ADR 0017, D2).
|
||||||
|
deprecated,
|
||||||
|
|
||||||
|
/// GitHub signs nobody in here any more: its time ended.
|
||||||
|
ended,
|
||||||
|
|
||||||
/// The instance refused it otherwise: it offers no GitHub sign-in, or the
|
/// The instance refused it otherwise: it offers no GitHub sign-in, or the
|
||||||
/// person already has a different GitHub account.
|
/// person already has a different GitHub account.
|
||||||
refused,
|
refused,
|
||||||
|
|
@ -15,3 +28,17 @@ final class const GitHubSignInFailed(final GitHubSignInProblem problem)
|
||||||
@override
|
@override
|
||||||
String toString() => 'GitHubSignInFailed(${problem.name})';
|
String toString() => 'GitHubSignInFailed(${problem.name})';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What an instance offers of GitHub, from one look at its basics.
|
||||||
|
final class const GitHubOffer({
|
||||||
|
/// Whether people sign in with GitHub here.
|
||||||
|
required final bool signIn,
|
||||||
|
|
||||||
|
/// Whether GitHub is added to people as a way in: not where they sign in
|
||||||
|
/// with a password (ADR 0017, D2), where only those who have it already
|
||||||
|
/// sign in with it.
|
||||||
|
required final bool linking,
|
||||||
|
}) with Equatable {
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [signIn, linking];
|
||||||
|
}
|
||||||
|
|
|
||||||
24
packages/halkyon_app/lib/src/auth/widget/github_texts.dart
Normal file
24
packages/halkyon_app/lib/src/auth/widget/github_texts.dart
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import 'package:halkyon_app/l10n/halkyon_app_localizations.dart';
|
||||||
|
import 'package:halkyon_app/src/auth/model/github_sign_in.dart';
|
||||||
|
|
||||||
|
/// What [problem] says to the person signing in with GitHub; null where the
|
||||||
|
/// screen's own "Couldn't sign in with GitHub." says it (no problem known,
|
||||||
|
/// or one the instance gave no reason of its own for).
|
||||||
|
///
|
||||||
|
/// For an [anonymous] person, [GitHubSignInProblem.deprecated] means the
|
||||||
|
/// GitHub account is nobody's here (the instance says first when it is
|
||||||
|
/// someone's), so the password that keeps their account is offered in its
|
||||||
|
/// place.
|
||||||
|
String? gitHubProblemText(
|
||||||
|
HalkyonAppLocalizations l10n,
|
||||||
|
GitHubSignInProblem? problem, {
|
||||||
|
required bool anonymous,
|
||||||
|
}) => switch (problem) {
|
||||||
|
GitHubSignInProblem.unknownAccount => l10n.startNoGitHubAccount,
|
||||||
|
GitHubSignInProblem.usePassword => l10n.gitHubUsePassword,
|
||||||
|
GitHubSignInProblem.deprecated when anonymous =>
|
||||||
|
l10n.gitHubNoAccountSetUpPassword,
|
||||||
|
GitHubSignInProblem.deprecated => l10n.gitHubDeprecated,
|
||||||
|
GitHubSignInProblem.ended => l10n.gitHubEnded,
|
||||||
|
GitHubSignInProblem.refused || null => null,
|
||||||
|
};
|
||||||
|
|
@ -182,10 +182,16 @@ class _StartScreenState() extends State<StartScreen> {
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
switch (problem) {
|
switch (problem) {
|
||||||
// Whole sentences: the app's names where.
|
// Whole sentences: where passwords are offered,
|
||||||
|
// GitHub is added to nobody (ADR 0017), so an
|
||||||
|
// account is created in its place; elsewhere the
|
||||||
|
// app's names where GitHub is added.
|
||||||
SignedOutProblem.noGitHubAccount =>
|
SignedOutProblem.noGitHubAccount =>
|
||||||
'${l10n.startNoGitHubAccount} '
|
'${l10n.startNoGitHubAccount} '
|
||||||
'${AppFrameScope.of(context).thenAddGitHub(context)}',
|
'${password ? l10n.gitHubCreateAccountInstead : AppFrameScope.of(context).thenAddGitHub(context)}',
|
||||||
|
SignedOutProblem.usePassword =>
|
||||||
|
l10n.gitHubUsePassword,
|
||||||
|
SignedOutProblem.gitHubEnded => l10n.gitHubEnded,
|
||||||
SignedOutProblem.failed => l10n.startGitHubFailed,
|
SignedOutProblem.failed => l10n.startGitHubFailed,
|
||||||
},
|
},
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'dart:async';
|
||||||
import 'package:halkyon_app/src/auth/data/auth_gateway.dart';
|
import 'package:halkyon_app/src/auth/data/auth_gateway.dart';
|
||||||
import 'package:halkyon_app/src/auth/model/auth_status.dart';
|
import 'package:halkyon_app/src/auth/model/auth_status.dart';
|
||||||
import 'package:halkyon_app/src/auth/model/github_link_outcome.dart';
|
import 'package:halkyon_app/src/auth/model/github_link_outcome.dart';
|
||||||
|
import 'package:halkyon_app/src/auth/model/github_sign_in.dart';
|
||||||
import 'package:halkyon_app/src/auth/model/session_grant.dart';
|
import 'package:halkyon_app/src/auth/model/session_grant.dart';
|
||||||
import 'package:halkyon_app/src/auth/model/sign_in_ways.dart';
|
import 'package:halkyon_app/src/auth/model/sign_in_ways.dart';
|
||||||
|
|
||||||
|
|
@ -34,12 +35,20 @@ class FakeAuthGateway({var AuthStatus _status = const SignedOut()})
|
||||||
/// Whether the instance offers GitHub sign-in.
|
/// Whether the instance offers GitHub sign-in.
|
||||||
bool offersGitHub = true;
|
bool offersGitHub = true;
|
||||||
|
|
||||||
|
/// Whether the instance adds GitHub to people as a way in: not where they
|
||||||
|
/// sign in with a password (ADR 0017).
|
||||||
|
bool linksGitHub = true;
|
||||||
|
|
||||||
/// How the next GitHub links end.
|
/// How the next GitHub links end.
|
||||||
GitHubLinkOutcome gitHubLinkOutcome = GitHubLinkOutcome.linked;
|
GitHubLinkOutcome gitHubLinkOutcome = GitHubLinkOutcome.linked;
|
||||||
|
|
||||||
/// Thrown by the next GitHub sign-ins while set.
|
/// Thrown by the next GitHub sign-ins while set.
|
||||||
Exception? gitHubError;
|
Exception? gitHubError;
|
||||||
|
|
||||||
|
/// Thrown by the next switches to a GitHub person while set, once a link
|
||||||
|
/// found the GitHub account taken.
|
||||||
|
Exception? gitHubSwitchError;
|
||||||
|
|
||||||
/// How often this device switched to a GitHub person.
|
/// How often this device switched to a GitHub person.
|
||||||
int gitHubSwitches = 0;
|
int gitHubSwitches = 0;
|
||||||
|
|
||||||
|
|
@ -93,8 +102,8 @@ class FakeAuthGateway({var AuthStatus _status = const SignedOut()})
|
||||||
void revokeSession() => _set(const SignedOut());
|
void revokeSession() => _set(const SignedOut());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> gitHubClientId() async =>
|
Future<GitHubOffer> gitHubOffer() async =>
|
||||||
offersGitHub ? 'github-client-id' : null;
|
GitHubOffer(signIn: offersGitHub, linking: offersGitHub && linksGitHub);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<GitHubLinkOutcome> linkGitHub() async {
|
Future<GitHubLinkOutcome> linkGitHub() async {
|
||||||
|
|
@ -120,6 +129,9 @@ class FakeAuthGateway({var AuthStatus _status = const SignedOut()})
|
||||||
if (!await _authorize()) {
|
if (!await _authorize()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (gitHubSwitchError case final error?) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
gitHubSwitches++;
|
gitHubSwitches++;
|
||||||
gitHubPeople.add('github-person');
|
gitHubPeople.add('github-person');
|
||||||
_set(const SignedIn(authUserId: 'github-person', isIdentified: true));
|
_set(const SignedIn(authUserId: 'github-person', isIdentified: true));
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,21 @@ void main() {
|
||||||
await expectLater(bloc.stream, emits(const GitHubSignInStateUnavailable()));
|
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 {
|
test('links the GitHub account to the person', () async {
|
||||||
final bloc = await ready();
|
final bloc = await ready();
|
||||||
|
|
||||||
|
|
@ -228,6 +243,60 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 {
|
test('says so when signing in fails', () async {
|
||||||
final bloc = await ready();
|
final bloc = await ready();
|
||||||
gateway.gitHubError = Exception('No connection');
|
gateway.gitHubError = Exception('No connection');
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,37 @@ void main() {
|
||||||
expect(gateway.gitHubSwitches, 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);
|
||||||
|
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.signIns, 0);
|
||||||
|
expect(gateway.gitHubSwitches, 0);
|
||||||
|
});
|
||||||
|
|
||||||
test('a session the server ended asks how to start too', () async {
|
test('a session the server ended asks how to start too', () async {
|
||||||
const stored = SignedIn(authUserId: 'github-1', isIdentified: true);
|
const stored = SignedIn(authUserId: 'github-1', isIdentified: true);
|
||||||
start(status: stored);
|
start(status: stored);
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,12 @@ final class _MemoryStorage() implements ClientAuthSuccessStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An instance's halkyon_base as the network shows it: anonymous sign-ins
|
/// An instance's halkyon_base as the network shows it: anonymous sign-ins
|
||||||
/// get `anonymous-token`, its basics offer GitHub and passwords, and a
|
/// get `anonymous-token`, its basics offer GitHub (and passwords while
|
||||||
/// sign-up makes Ben, signed in with `ben-token`.
|
/// [passwords]), and a sign-up makes Ben, signed in with `ben-token`.
|
||||||
final class _Instance() {
|
final class _Instance() {
|
||||||
|
/// Whether its basics offer password sign-in.
|
||||||
|
bool passwords = false;
|
||||||
|
|
||||||
static final anonymous = UuidValue.withValidation(
|
static final anonymous = UuidValue.withValidation(
|
||||||
'0191f0c4-1111-7000-8000-000000000001',
|
'0191f0c4-1111-7000-8000-000000000001',
|
||||||
);
|
);
|
||||||
|
|
@ -50,6 +53,7 @@ final class _Instance() {
|
||||||
emailSignIn: true,
|
emailSignIn: true,
|
||||||
sourceCodeUrl: 'https://code.example.org/learn',
|
sourceCodeUrl: 'https://code.example.org/learn',
|
||||||
minimumAppBuild: 1,
|
minimumAppBuild: 1,
|
||||||
|
passwordSignIn: passwords,
|
||||||
).toJson(),
|
).toJson(),
|
||||||
),
|
),
|
||||||
200,
|
200,
|
||||||
|
|
@ -116,8 +120,44 @@ void main() {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
test('tells what the instance offers from its basics', () async {
|
test('tells what the instance offers from its basics: GitHub sign-in, '
|
||||||
expect(await gateway.gitHubClientId(), 'github-client');
|
'and linking GitHub to people only where passwords are not', () 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',
|
||||||
|
),
|
||||||
|
hasLength(2),
|
||||||
|
reason: 'one read of the basics each',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("puts the instance's refusals of GitHub as the app's problems", () {
|
||||||
|
expect(
|
||||||
|
{
|
||||||
|
for (final reason in GitHubSignInRefusal.values)
|
||||||
|
reason: gitHubSignInProblemOf(reason),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
GitHubSignInRefusal.unavailable: GitHubSignInProblem.refused,
|
||||||
|
GitHubSignInRefusal.takenByAnotherPerson: GitHubSignInProblem.refused,
|
||||||
|
GitHubSignInRefusal.otherGitHubAccountLinked:
|
||||||
|
GitHubSignInProblem.refused,
|
||||||
|
GitHubSignInRefusal.unknownAccount: GitHubSignInProblem.unknownAccount,
|
||||||
|
GitHubSignInRefusal.usePassword: GitHubSignInProblem.usePassword,
|
||||||
|
GitHubSignInRefusal.deprecated: GitHubSignInProblem.deprecated,
|
||||||
|
GitHubSignInRefusal.ended: GitHubSignInProblem.ended,
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a session granted elsewhere (a sign-up with a password, without '
|
test('a session granted elsewhere (a sign-up with a password, without '
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue