chore(shorebird_cli): remove shorebird account create and shorebird account usage (#886)

This commit is contained in:
Felix Angelov
2023-07-21 10:48:38 -05:00
committed by GitHub
parent 08b16d6a33
commit b6eb925c08
10 changed files with 0 additions and 718 deletions
@@ -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);
});
});
}