From 01d33b682688bb34d2c39175f14bb72dbb529e28 Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Fri, 16 Jun 2023 13:09:54 -0700 Subject: [PATCH] feat(shorebird_cli): add `shorebird account usage` command (#680) --- .../lib/src/code_push_client_wrapper.dart | 14 ++ .../lib/src/commands/account/account.dart | 1 + .../src/commands/account/account_command.dart | 1 + .../account/account_usage_command.dart | 77 +++++++++++ .../src/code_push_client_wrapper_test.dart | 57 +++++++++ .../account/account_usage_command_test.dart | 120 ++++++++++++++++++ 6 files changed, 270 insertions(+) create mode 100644 packages/shorebird_cli/lib/src/commands/account/account_usage_command.dart create mode 100644 packages/shorebird_cli/test/src/commands/account/account_usage_command_test.dart diff --git a/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart b/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart index 349a6229..f43ba9e5 100644 --- a/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart +++ b/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart @@ -527,4 +527,18 @@ aar artifact already exists, continuing...''', await promotePatch(patchId: patch.id, channel: channel); } + + Future> getUsage() async { + final progress = logger.progress('Fetching usage'); + final List? appsUsage; + try { + appsUsage = await codePushClient.getUsage(); + progress.complete(); + } catch (error) { + progress.fail(error.toString()); + exit(ExitCode.software.code); + } + + return appsUsage; + } } diff --git a/packages/shorebird_cli/lib/src/commands/account/account.dart b/packages/shorebird_cli/lib/src/commands/account/account.dart index 9479098c..356a67cd 100644 --- a/packages/shorebird_cli/lib/src/commands/account/account.dart +++ b/packages/shorebird_cli/lib/src/commands/account/account.dart @@ -1,3 +1,4 @@ export 'account_command.dart'; +export 'account_usage_command.dart'; export 'create_account_command.dart'; export 'subscribe_account_command.dart'; diff --git a/packages/shorebird_cli/lib/src/commands/account/account_command.dart b/packages/shorebird_cli/lib/src/commands/account/account_command.dart index 81110579..1146794f 100644 --- a/packages/shorebird_cli/lib/src/commands/account/account_command.dart +++ b/packages/shorebird_cli/lib/src/commands/account/account_command.dart @@ -8,6 +8,7 @@ import 'package:shorebird_cli/src/commands/commands.dart'; class AccountCommand extends ShorebirdCommand { /// {@macro account_command} AccountCommand() { + addSubcommand(AccountUsageCommand()); addSubcommand(CreateAccountCommand()); addSubcommand(SubscribeAccountCommand()); } diff --git a/packages/shorebird_cli/lib/src/commands/account/account_usage_command.dart b/packages/shorebird_cli/lib/src/commands/account/account_usage_command.dart new file mode 100644 index 00000000..66390e8f --- /dev/null +++ b/packages/shorebird_cli/lib/src/commands/account/account_usage_command.dart @@ -0,0 +1,77 @@ +import 'dart:async'; + +import 'package:barbecue/barbecue.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 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 List { + String prettyPrint() { + const cellStyle = CellStyle( + paddingLeft: 1, + paddingRight: 1, + borderBottom: true, + borderTop: true, + borderLeft: true, + borderRight: true, + ); + var totalPatchInstalls = 0; + for (final appUsage in this) { + for (final platformUsage in appUsage.platforms) { + for (final archUsage in platformUsage.arches) { + for (final patchUsage in archUsage.patches) { + totalPatchInstalls += patchUsage.installCount; + } + } + } + } + + return Table( + cellStyle: cellStyle, + header: const TableSection( + rows: [ + Row(cells: [Cell('Total Patch Installs')]) + ], + ), + body: TableSection( + rows: [ + Row(cells: [Cell('$totalPatchInstalls')]) + ], + ), + ).render(); + } +} diff --git a/packages/shorebird_cli/test/src/code_push_client_wrapper_test.dart b/packages/shorebird_cli/test/src/code_push_client_wrapper_test.dart index be0b353d..a3509b98 100644 --- a/packages/shorebird_cli/test/src/code_push_client_wrapper_test.dart +++ b/packages/shorebird_cli/test/src/code_push_client_wrapper_test.dart @@ -1557,6 +1557,63 @@ 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 { + const usage = [ + AppUsage( + id: 'test-app-id', + platforms: [ + PlatformUsage( + name: 'android', + arches: [ + ArchUsage( + name: 'aarch64', + patches: [ + PatchUsage(id: 0, installCount: 10), + PatchUsage(id: 1, installCount: 10) + ], + ), + ArchUsage( + name: 'arm', + patches: [ + PatchUsage(id: 0, installCount: 10), + PatchUsage(id: 1, installCount: 10) + ], + ), + ArchUsage( + name: 'x86', + patches: [ + PatchUsage(id: 0, installCount: 1), + PatchUsage(id: 1, installCount: 1) + ], + ) + ], + ) + ], + ), + ]; + when(() => codePushClient.getUsage()).thenAnswer((_) async => usage); + + await expectLater( + runWithOverrides(codePushClientWrapper.getUsage), + completion(equals(usage)), + ); + + verify(() => progress.complete()).called(1); + }); + }); }); }); } diff --git a/packages/shorebird_cli/test/src/commands/account/account_usage_command_test.dart b/packages/shorebird_cli/test/src/commands/account/account_usage_command_test.dart new file mode 100644 index 00000000..0fae0745 --- /dev/null +++ b/packages/shorebird_cli/test/src/commands/account/account_usage_command_test.dart @@ -0,0 +1,120 @@ +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 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 = [ + const AppUsage( + id: 'test-app-id', + platforms: [ + PlatformUsage( + name: 'android', + arches: [ + ArchUsage( + name: 'aarch64', + patches: [ + PatchUsage(id: 0, installCount: 10), + PatchUsage(id: 1, installCount: 10) + ], + ), + ArchUsage( + name: 'arm', + patches: [ + PatchUsage(id: 0, installCount: 10), + PatchUsage(id: 1, installCount: 10) + ], + ), + ArchUsage( + name: 'x86', + patches: [ + PatchUsage(id: 0, installCount: 1), + PatchUsage(id: 1, installCount: 1) + ], + ) + ], + ) + ], + ), + ]; + 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(''' +┌──────────────────────┐ +│ Total Patch Installs │ +├──────────────────────┤ +│ 42 │ +└──────────────────────┘'''), + ).called(1); + }); + }); +}