chore(shorebird_cli): remove shorebird account create and shorebird account usage (#886)
This commit is contained in:
@@ -237,42 +237,6 @@ class Auth {
|
||||
|
||||
void logout() => _clearCredentials();
|
||||
|
||||
Future<User> signUp({
|
||||
required void Function(String) authPrompt,
|
||||
required String Function() namePrompt,
|
||||
}) async {
|
||||
if (_credentials != null) {
|
||||
throw UserAlreadyLoggedInException(email: _credentials!.email!);
|
||||
}
|
||||
|
||||
final client = http.Client();
|
||||
final User newUser;
|
||||
try {
|
||||
_credentials = await _obtainAccessCredentials(
|
||||
_clientId,
|
||||
_scopes,
|
||||
client,
|
||||
authPrompt,
|
||||
);
|
||||
|
||||
final codePushClient = _buildCodePushClient(httpClient: this.client);
|
||||
|
||||
final existingUser = await codePushClient.getCurrentUser();
|
||||
if (existingUser != null) {
|
||||
throw UserAlreadyExistsException(existingUser);
|
||||
}
|
||||
|
||||
newUser = await codePushClient.createUser(name: namePrompt());
|
||||
|
||||
_email = newUser.email;
|
||||
_flushCredentials(_credentials!);
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
|
||||
return newUser;
|
||||
}
|
||||
|
||||
oauth2.AccessCredentials? _credentials;
|
||||
|
||||
String? _email;
|
||||
@@ -356,14 +320,3 @@ class UserNotFoundException implements Exception {
|
||||
/// credentials.
|
||||
final String email;
|
||||
}
|
||||
|
||||
/// {@template user_already_exists_exception}
|
||||
/// Thrown when an attempt to create a User object results in a 409.
|
||||
/// {@endtemplate}
|
||||
class UserAlreadyExistsException implements Exception {
|
||||
/// {@macro user_already_exists_exception}
|
||||
UserAlreadyExistsException(this.user);
|
||||
|
||||
/// The existing user.
|
||||
final User user;
|
||||
}
|
||||
|
||||
@@ -615,16 +615,4 @@ aar artifact already exists, continuing...''',
|
||||
|
||||
await promotePatch(appId: appId, patchId: patch.id, channel: channel);
|
||||
}
|
||||
|
||||
Future<GetUsageResponse> getUsage() async {
|
||||
final progress = logger.progress('Fetching usage');
|
||||
try {
|
||||
final usage = await codePushClient.getUsage();
|
||||
progress.complete();
|
||||
return usage;
|
||||
} catch (error) {
|
||||
progress.fail(error.toString());
|
||||
exit(ExitCode.software.code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
export 'account_command.dart';
|
||||
export 'account_usage_command.dart';
|
||||
export 'create_account_command.dart';
|
||||
export 'downgrade_account_command.dart';
|
||||
export 'upgrade_account_command.dart';
|
||||
|
||||
@@ -8,8 +8,6 @@ import 'package:shorebird_cli/src/commands/commands.dart';
|
||||
class AccountCommand extends ShorebirdCommand {
|
||||
/// {@macro account_command}
|
||||
AccountCommand() {
|
||||
addSubcommand(AccountUsageCommand());
|
||||
addSubcommand(CreateAccountCommand());
|
||||
addSubcommand(DowngradeAccountCommand());
|
||||
addSubcommand(UpgradeAccountCommand());
|
||||
}
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:barbecue/barbecue.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||
import 'package:shorebird_cli/src/command.dart';
|
||||
import 'package:shorebird_cli/src/logger.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_config_mixin.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_validation_mixin.dart';
|
||||
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
|
||||
|
||||
/// {@template account_usage_command}
|
||||
/// `shorebird account usage`
|
||||
/// Get usage information for your Shorebird account.
|
||||
/// {@endtemplate}
|
||||
class AccountUsageCommand extends ShorebirdCommand
|
||||
with ShorebirdConfigMixin, ShorebirdValidationMixin {
|
||||
@override
|
||||
String get description => 'Get usage information for your Shorebird account.';
|
||||
|
||||
@override
|
||||
String get name => 'usage';
|
||||
|
||||
@override
|
||||
Future<int> run() async {
|
||||
try {
|
||||
await validatePreconditions(checkUserIsAuthenticated: true);
|
||||
} on PreconditionFailedException catch (e) {
|
||||
return e.exitCode.code;
|
||||
}
|
||||
|
||||
final usage = await codePushClientWrapper.getUsage();
|
||||
|
||||
logger
|
||||
..info('📈 Usage')
|
||||
..info(usage.prettyPrint());
|
||||
|
||||
return ExitCode.success.code;
|
||||
}
|
||||
}
|
||||
|
||||
extension on GetUsageResponse {
|
||||
String prettyPrint() {
|
||||
const cellStyle = CellStyle(
|
||||
paddingLeft: 1,
|
||||
paddingRight: 1,
|
||||
borderBottom: true,
|
||||
borderTop: true,
|
||||
borderLeft: true,
|
||||
borderRight: true,
|
||||
);
|
||||
var totalPatchInstalls = 0;
|
||||
for (final appUsage in apps) {
|
||||
totalPatchInstalls += appUsage.patchInstallCount;
|
||||
}
|
||||
|
||||
final remainingPatchInstalls = plan.patchInstallLimit == null
|
||||
? '∞'
|
||||
: '${plan.patchInstallLimit! - totalPatchInstalls}';
|
||||
|
||||
return '''
|
||||
|
||||
You are on the ${lightCyan.wrap(plan.name)} plan.
|
||||
|
||||
${Table(
|
||||
cellStyle: cellStyle,
|
||||
header: const TableSection(
|
||||
rows: [
|
||||
Row(
|
||||
cells: [
|
||||
Cell('App'),
|
||||
Cell('Patch Installs'),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
body: TableSection(
|
||||
rows: [
|
||||
for (final appUsage in apps)
|
||||
Row(
|
||||
cells: [
|
||||
Cell(appUsage.name),
|
||||
Cell('${appUsage.patchInstallCount}'),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
cells: [
|
||||
const Cell('Total'),
|
||||
Cell('$totalPatchInstalls'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
).render()}
|
||||
|
||||
${styleBold.wrap('${lightCyan.wrap(remainingPatchInstalls)} patch installs remaining in the current billing period.')}
|
||||
|
||||
Current Billing Period: ${lightCyan.wrap(DateFormat.yMMMd().format(currentPeriodStart))} - ${lightCyan.wrap(DateFormat.yMMMd().format(currentPeriodEnd))}
|
||||
Month-to-date cost: ${lightCyan.wrap(currentPeriodCost.toString())}
|
||||
|
||||
${styleBold.wrap('*Usage data is not reported in real-time and may be delayed by up to 48 hours.')}''';
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:shorebird_cli/src/auth/auth.dart';
|
||||
import 'package:shorebird_cli/src/command.dart';
|
||||
import 'package:shorebird_cli/src/logger.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_config_mixin.dart';
|
||||
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
|
||||
|
||||
/// {@template create_account_command}
|
||||
/// `shorebird account create`
|
||||
/// Create a new Shorebird account.
|
||||
/// {@endtemplate}
|
||||
class CreateAccountCommand extends ShorebirdCommand with ShorebirdConfigMixin {
|
||||
@override
|
||||
String get description => 'Create a new Shorebird account.';
|
||||
|
||||
@override
|
||||
String get name => 'create';
|
||||
|
||||
@override
|
||||
Future<int> run() async {
|
||||
final User newUser;
|
||||
try {
|
||||
newUser = await auth.signUp(
|
||||
authPrompt: authPrompt,
|
||||
namePrompt: namePrompt,
|
||||
);
|
||||
} on UserAlreadyLoggedInException catch (error) {
|
||||
logger
|
||||
..info('You are already logged in as <${error.email}>.')
|
||||
..info(
|
||||
'Run ${lightCyan.wrap('shorebird logout')} to log out and try again.',
|
||||
);
|
||||
return ExitCode.success.code;
|
||||
} on UserAlreadyExistsException catch (error) {
|
||||
// TODO(bryanoltman): change this message based on the user's subscription
|
||||
// status.
|
||||
logger.info('''
|
||||
You already have an account, ${error.user.displayName}!
|
||||
To login, run ${lightCyan.wrap('shorebird login')}.
|
||||
''');
|
||||
return ExitCode.success.code;
|
||||
} catch (error) {
|
||||
logger.err(error.toString());
|
||||
return ExitCode.software.code;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
'''
|
||||
|
||||
🎉 ${lightGreen.wrap('Welcome to Shorebird, ${newUser.displayName}!')}
|
||||
🔑 Credentials are stored in ${lightCyan.wrap(auth.credentialsFilePath)}.
|
||||
🚪 To logout, use: "${lightCyan.wrap('shorebird logout')}".
|
||||
|
||||
Your current plan is ${lightCyan.wrap('Hobby')}.
|
||||
To upgrade, run ${lightCyan.wrap('shorebird account upgrade')}.
|
||||
|
||||
Please let us know via Discord if we can help!
|
||||
${lightCyan.wrap('https://discord.gg/shorebird')}.
|
||||
''',
|
||||
);
|
||||
return ExitCode.success.code;
|
||||
}
|
||||
|
||||
void authPrompt(String url) {
|
||||
logger.info('''
|
||||
Shorebird currently requires a Google account for authentication. If you'd like to use a different kind of auth, please let us know: ${lightCyan.wrap('https://github.com/shorebirdtech/shorebird/issues/335')}.
|
||||
|
||||
Follow the link below to authenticate:
|
||||
|
||||
${styleBold.wrap(styleUnderlined.wrap(lightCyan.wrap(url)))}
|
||||
|
||||
Waiting for your authorization...''');
|
||||
}
|
||||
|
||||
String namePrompt() => logger.prompt('''
|
||||
Tell us your name to finish creating your account:''');
|
||||
}
|
||||
@@ -41,7 +41,6 @@ void main() {
|
||||
const idToken =
|
||||
'''eyJhbGciOiJSUzI1NiIsImN0eSI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAZW1haWwuY29tIn0.pD47BhF3MBLyIpfsgWCzP9twzC1HJxGukpcR36DqT6yfiOMHTLcjDbCjRLAnklWEHiT0BQTKTfhs8IousU90Fm5bVKObudfKu8pP5iZZ6Ls4ohDjTrXky9j3eZpZjwv8CnttBVgRfMJG-7YASTFRYFcOLUpnb4Zm5R6QdoCDUYg''';
|
||||
const email = 'test@email.com';
|
||||
const name = 'Jane Doe';
|
||||
const user = User(id: 42, email: email);
|
||||
const refreshToken = '';
|
||||
const scopes = <String>[];
|
||||
@@ -403,78 +402,6 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('signUp', () {
|
||||
test(
|
||||
'should set the email when claims are valid and user is successfully '
|
||||
'created', () async {
|
||||
when(() => codePushClient.getCurrentUser())
|
||||
.thenAnswer((_) async => null);
|
||||
when(() => codePushClient.createUser(name: any(named: 'name')))
|
||||
.thenAnswer((_) async => user);
|
||||
|
||||
final newUser = await auth.signUp(
|
||||
authPrompt: (_) {},
|
||||
namePrompt: () => name,
|
||||
);
|
||||
expect(user, newUser);
|
||||
expect(auth.email, email);
|
||||
expect(auth.isAuthenticated, isTrue);
|
||||
expect(buildAuth().email, email);
|
||||
expect(buildAuth().isAuthenticated, isTrue);
|
||||
});
|
||||
|
||||
test('throws UserAlreadyLoggedInException if user is authenticated',
|
||||
() async {
|
||||
writeCredentials();
|
||||
auth = buildAuth();
|
||||
|
||||
await expectLater(
|
||||
auth.signUp(
|
||||
authPrompt: (_) {},
|
||||
namePrompt: () => name,
|
||||
),
|
||||
throwsA(isA<UserAlreadyLoggedInException>()),
|
||||
);
|
||||
|
||||
expect(auth.email, isNotNull);
|
||||
expect(auth.isAuthenticated, isTrue);
|
||||
});
|
||||
|
||||
test('throws UserAlreadyExistsException if user already exists',
|
||||
() async {
|
||||
when(() => codePushClient.getCurrentUser())
|
||||
.thenAnswer((_) async => user);
|
||||
|
||||
await expectLater(
|
||||
auth.signUp(
|
||||
authPrompt: (_) {},
|
||||
namePrompt: () => name,
|
||||
),
|
||||
throwsA(isA<UserAlreadyExistsException>()),
|
||||
);
|
||||
verifyNever(() => codePushClient.createUser(name: any(named: 'name')));
|
||||
expect(auth.email, isNull);
|
||||
expect(auth.isAuthenticated, isFalse);
|
||||
});
|
||||
|
||||
test('throws exception if createUser fails', () async {
|
||||
when(() => codePushClient.getCurrentUser())
|
||||
.thenAnswer((_) async => null);
|
||||
when(() => codePushClient.createUser(name: any(named: 'name')))
|
||||
.thenThrow(Exception('oh no!'));
|
||||
|
||||
await expectLater(
|
||||
auth.signUp(
|
||||
authPrompt: (_) {},
|
||||
namePrompt: () => name,
|
||||
),
|
||||
throwsA(isA<Exception>()),
|
||||
);
|
||||
expect(auth.email, isNull);
|
||||
expect(auth.isAuthenticated, isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('logout', () {
|
||||
test('clears session and wipes state', () async {
|
||||
await auth.login((_) {});
|
||||
|
||||
@@ -1849,48 +1849,6 @@ Please bump your version number and try again.''',
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('getUsage', () {
|
||||
test('exits with code 70 when getUsage throws an exception', () async {
|
||||
when(() => codePushClient.getUsage()).thenThrow(Exception('oh no!'));
|
||||
|
||||
await expectLater(
|
||||
() => runWithOverrides(codePushClientWrapper.getUsage),
|
||||
exitsWithCode(ExitCode.software),
|
||||
);
|
||||
|
||||
verify(() => progress.fail(any(that: contains('oh no!')))).called(1);
|
||||
});
|
||||
|
||||
test('returns usage when succeeds', () async {
|
||||
final usage = GetUsageResponse(
|
||||
plan: ShorebirdPlan(
|
||||
name: 'Hobby',
|
||||
monthlyCost: Money.fromIntWithCurrency(0, usd),
|
||||
patchInstallLimit: 1000,
|
||||
maxTeamSize: 1,
|
||||
),
|
||||
apps: const [
|
||||
AppUsage(
|
||||
id: 'test-app-id',
|
||||
name: 'test app',
|
||||
patchInstallCount: 42,
|
||||
),
|
||||
],
|
||||
currentPeriodCost: Money.fromIntWithCurrency(0, usd),
|
||||
currentPeriodStart: DateTime(2023),
|
||||
currentPeriodEnd: DateTime(2023, 2),
|
||||
);
|
||||
when(() => codePushClient.getUsage()).thenAnswer((_) async => usage);
|
||||
|
||||
await expectLater(
|
||||
runWithOverrides(codePushClientWrapper.getUsage),
|
||||
completion(equals(usage)),
|
||||
);
|
||||
|
||||
verify(() => progress.complete()).called(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:scoped/scoped.dart';
|
||||
import 'package:shorebird_cli/src/auth/auth.dart';
|
||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||
import 'package:shorebird_cli/src/commands/account/account.dart';
|
||||
import 'package:shorebird_cli/src/logger.dart';
|
||||
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class _MockAuth extends Mock implements Auth {}
|
||||
|
||||
class _MockCodePushClientWrapper extends Mock
|
||||
implements CodePushClientWrapper {}
|
||||
|
||||
class _MockLogger extends Mock implements Logger {}
|
||||
|
||||
class _MockProgress extends Mock implements Progress {}
|
||||
|
||||
void main() {
|
||||
late Auth auth;
|
||||
late CodePushClientWrapper codePushClientWrapper;
|
||||
late Logger logger;
|
||||
late Progress progress;
|
||||
|
||||
late AccountUsageCommand command;
|
||||
|
||||
group(AccountUsageCommand, () {
|
||||
R runWithOverrides<R>(R Function() body) {
|
||||
return runScoped(
|
||||
body,
|
||||
values: {
|
||||
authRef.overrideWith(() => auth),
|
||||
loggerRef.overrideWith(() => logger),
|
||||
codePushClientWrapperRef.overrideWith(() => codePushClientWrapper)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
auth = _MockAuth();
|
||||
codePushClientWrapper = _MockCodePushClientWrapper();
|
||||
logger = _MockLogger();
|
||||
progress = _MockProgress();
|
||||
|
||||
when(() => auth.isAuthenticated).thenReturn(true);
|
||||
when(() => logger.progress(any())).thenReturn(progress);
|
||||
|
||||
command = runWithOverrides(AccountUsageCommand.new);
|
||||
});
|
||||
|
||||
test('has a description', () {
|
||||
expect(command.description, isNotEmpty);
|
||||
});
|
||||
|
||||
test('exits with code 67 when user is not logged in', () async {
|
||||
when(() => auth.isAuthenticated).thenReturn(false);
|
||||
|
||||
final result = await runWithOverrides(command.run);
|
||||
|
||||
expect(result, ExitCode.noUser.code);
|
||||
|
||||
verify(
|
||||
() => logger.err(any(that: contains('You must be logged in to run'))),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('exits with code 0 when usage is fetched.', () async {
|
||||
final usage = GetUsageResponse(
|
||||
plan: ShorebirdPlan(
|
||||
name: 'Team',
|
||||
monthlyCost: Money.fromIntWithCurrency(2000, usd),
|
||||
patchInstallLimit: 20000,
|
||||
maxTeamSize: 1,
|
||||
),
|
||||
apps: const [
|
||||
AppUsage(
|
||||
id: 'test-app-id',
|
||||
name: 'test app 2',
|
||||
patchInstallCount: 42,
|
||||
),
|
||||
AppUsage(
|
||||
id: 'test-app-id',
|
||||
name: 'test app 2',
|
||||
patchInstallCount: 42,
|
||||
),
|
||||
],
|
||||
currentPeriodCost: Money.fromIntWithCurrency(2000, usd),
|
||||
currentPeriodStart: DateTime(2023),
|
||||
currentPeriodEnd: DateTime(2023, 2),
|
||||
);
|
||||
when(
|
||||
() => codePushClientWrapper.getUsage(),
|
||||
).thenAnswer((_) async => usage);
|
||||
|
||||
final result = await runWithOverrides(command.run);
|
||||
|
||||
expect(result, ExitCode.success.code);
|
||||
verify(() => logger.info('📈 Usage')).called(1);
|
||||
verify(
|
||||
() => logger.info(
|
||||
any(
|
||||
that: contains('''
|
||||
|
||||
You are on the ${lightCyan.wrap('Team')} plan.
|
||||
|
||||
┌────────────┬────────────────┐
|
||||
│ App │ Patch Installs │
|
||||
├────────────┼────────────────┤
|
||||
│ test app 2 │ 42 │
|
||||
├────────────┼────────────────┤
|
||||
│ test app 2 │ 42 │
|
||||
├────────────┼────────────────┤
|
||||
│ Total │ 84 │
|
||||
└────────────┴────────────────┘
|
||||
|
||||
${styleBold.wrap('${lightCyan.wrap('19916')} patch installs remaining in the current billing period.')}
|
||||
|
||||
Current Billing Period: ${lightCyan.wrap(DateFormat.yMMMd().format(usage.currentPeriodStart))} - ${lightCyan.wrap(DateFormat.yMMMd().format(usage.currentPeriodEnd))}
|
||||
Month-to-date cost: ${lightCyan.wrap(r'$20.00')}
|
||||
|
||||
${styleBold.wrap('*Usage data is not reported in real-time and may be delayed by up to 48 hours.')}'''),
|
||||
),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('exits with code 0 when usage is fetched (unlimited).', () async {
|
||||
final usage = GetUsageResponse(
|
||||
plan: ShorebirdPlan(
|
||||
name: 'Hobby',
|
||||
monthlyCost: Money.fromIntWithCurrency(0, usd),
|
||||
patchInstallLimit: null,
|
||||
maxTeamSize: 1,
|
||||
),
|
||||
apps: const [
|
||||
AppUsage(
|
||||
id: 'test-app-id',
|
||||
name: 'test app 2',
|
||||
patchInstallCount: 42,
|
||||
),
|
||||
AppUsage(
|
||||
id: 'test-app-id',
|
||||
name: 'test app 2',
|
||||
patchInstallCount: 42,
|
||||
),
|
||||
],
|
||||
currentPeriodCost: Money.fromIntWithCurrency(0, usd),
|
||||
currentPeriodStart: DateTime(2023),
|
||||
currentPeriodEnd: DateTime(2023, 2),
|
||||
);
|
||||
when(
|
||||
() => codePushClientWrapper.getUsage(),
|
||||
).thenAnswer((_) async => usage);
|
||||
|
||||
final result = await runWithOverrides(command.run);
|
||||
|
||||
expect(result, ExitCode.success.code);
|
||||
verify(() => logger.info('📈 Usage')).called(1);
|
||||
verify(
|
||||
() => logger.info(
|
||||
any(
|
||||
that: contains('''
|
||||
|
||||
You are on the ${lightCyan.wrap('Hobby')} plan.
|
||||
|
||||
┌────────────┬────────────────┐
|
||||
│ App │ Patch Installs │
|
||||
├────────────┼────────────────┤
|
||||
│ test app 2 │ 42 │
|
||||
├────────────┼────────────────┤
|
||||
│ test app 2 │ 42 │
|
||||
├────────────┼────────────────┤
|
||||
│ Total │ 84 │
|
||||
└────────────┴────────────────┘
|
||||
|
||||
${styleBold.wrap('${lightCyan.wrap('∞')} patch installs remaining in the current billing period.')}
|
||||
|
||||
Current Billing Period: ${lightCyan.wrap(DateFormat.yMMMd().format(usage.currentPeriodStart))} - ${lightCyan.wrap(DateFormat.yMMMd().format(usage.currentPeriodEnd))}
|
||||
Month-to-date cost: ${lightCyan.wrap(r'$0.00')}
|
||||
|
||||
${styleBold.wrap('*Usage data is not reported in real-time and may be delayed by up to 48 hours.')}'''),
|
||||
),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:scoped/scoped.dart';
|
||||
import 'package:shorebird_cli/src/auth/auth.dart';
|
||||
import 'package:shorebird_cli/src/commands/account/account.dart';
|
||||
import 'package:shorebird_cli/src/logger.dart' hide logger;
|
||||
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class _MockAuth extends Mock implements Auth {}
|
||||
|
||||
class _MockHttpClient extends Mock implements http.Client {}
|
||||
|
||||
class _MockLogger extends Mock implements Logger {}
|
||||
|
||||
class _MockUser extends Mock implements User {}
|
||||
|
||||
void main() {
|
||||
group(CreateAccountCommand, () {
|
||||
const userName = 'John Doe';
|
||||
const email = 'tester@shorebird.dev';
|
||||
|
||||
late Auth auth;
|
||||
late http.Client httpClient;
|
||||
late Logger logger;
|
||||
late User user;
|
||||
|
||||
late CreateAccountCommand createAccountCommand;
|
||||
|
||||
R runWithOverrides<R>(R Function() body) {
|
||||
return runScoped(
|
||||
body,
|
||||
values: {
|
||||
authRef.overrideWith(() => auth),
|
||||
loggerRef.overrideWith(() => logger)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
auth = _MockAuth();
|
||||
httpClient = _MockHttpClient();
|
||||
logger = _MockLogger();
|
||||
user = _MockUser();
|
||||
|
||||
when(() => auth.client).thenReturn(httpClient);
|
||||
when(() => auth.credentialsFilePath).thenReturn('credentials.json');
|
||||
|
||||
when(() => logger.prompt(any())).thenReturn(userName);
|
||||
|
||||
when(() => user.displayName).thenReturn(userName);
|
||||
when(() => user.email).thenReturn(email);
|
||||
|
||||
createAccountCommand = runWithOverrides(CreateAccountCommand.new);
|
||||
});
|
||||
|
||||
test('has a description', () {
|
||||
expect(createAccountCommand.description, isNotEmpty);
|
||||
});
|
||||
|
||||
test('login prompt is correct', () {
|
||||
runWithOverrides(
|
||||
() => createAccountCommand.authPrompt('https://shorebird.dev'),
|
||||
);
|
||||
verify(
|
||||
() => logger.info('''
|
||||
Shorebird currently requires a Google account for authentication. If you'd like to use a different kind of auth, please let us know: ${lightCyan.wrap('https://github.com/shorebirdtech/shorebird/issues/335')}.
|
||||
|
||||
Follow the link below to authenticate:
|
||||
|
||||
${styleBold.wrap(styleUnderlined.wrap(lightCyan.wrap('https://shorebird.dev')))}
|
||||
|
||||
Waiting for your authorization...'''),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('namePrompt asks user for name', () {
|
||||
final name = runWithOverrides(() => createAccountCommand.namePrompt());
|
||||
expect(name, userName);
|
||||
verify(
|
||||
() => logger.prompt(
|
||||
'Tell us your name to finish creating your account:',
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('exits with code 0 if user is logged in', () async {
|
||||
when(
|
||||
() => auth.signUp(
|
||||
authPrompt: any(named: 'authPrompt'),
|
||||
namePrompt: any(named: 'namePrompt'),
|
||||
),
|
||||
).thenThrow(UserAlreadyLoggedInException(email: email));
|
||||
|
||||
final result = await runWithOverrides(createAccountCommand.run);
|
||||
|
||||
expect(result, ExitCode.success.code);
|
||||
|
||||
verify(
|
||||
() => logger.info(any(that: contains('You are already logged in '))),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test(
|
||||
'exits with code 0 and prints message and exits if user already has an '
|
||||
'account', () async {
|
||||
when(
|
||||
() => auth.signUp(
|
||||
authPrompt: any(named: 'authPrompt'),
|
||||
namePrompt: any(named: 'namePrompt'),
|
||||
),
|
||||
).thenThrow(UserAlreadyExistsException(user));
|
||||
|
||||
final result = await runWithOverrides(createAccountCommand.run);
|
||||
|
||||
expect(result, ExitCode.success.code);
|
||||
verify(
|
||||
() => logger.info(any(that: contains('You already have an account'))),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('exits with code 70 when signUp fails', () async {
|
||||
when(
|
||||
() => auth.signUp(
|
||||
authPrompt: any(named: 'authPrompt'),
|
||||
namePrompt: any(named: 'namePrompt'),
|
||||
),
|
||||
).thenThrow(Exception('login failed'));
|
||||
|
||||
final result = await runWithOverrides(createAccountCommand.run);
|
||||
|
||||
expect(result, ExitCode.software.code);
|
||||
verify(() => logger.err(any(that: contains('login failed')))).called(1);
|
||||
});
|
||||
|
||||
test('exits with code 0, creates account with name provided by user',
|
||||
() async {
|
||||
when(
|
||||
() => auth.signUp(
|
||||
authPrompt: any(named: 'authPrompt'),
|
||||
namePrompt: any(named: 'namePrompt'),
|
||||
),
|
||||
).thenAnswer((_) async => user);
|
||||
|
||||
final result = await runWithOverrides(createAccountCommand.run);
|
||||
|
||||
expect(result, ExitCode.success.code);
|
||||
verify(
|
||||
() => auth.signUp(
|
||||
authPrompt: any(named: 'authPrompt'),
|
||||
namePrompt: any(named: 'namePrompt'),
|
||||
),
|
||||
).called(1);
|
||||
verify(
|
||||
() => logger.info(
|
||||
any(
|
||||
that: stringContainsInOrder([
|
||||
'Welcome to Shorebird',
|
||||
userName,
|
||||
'shorebird account upgrade',
|
||||
]),
|
||||
),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user