feat(shorebird_cli): add shorebird account usage command (#680)

This commit is contained in:
Felix Angelov
2023-06-16 13:09:54 -07:00
committed by GitHub
parent 7f95a0a931
commit 01d33b6826
6 changed files with 270 additions and 0 deletions
@@ -527,4 +527,18 @@ aar artifact already exists, continuing...''',
await promotePatch(patchId: patch.id, channel: channel);
}
Future<List<AppUsage>> getUsage() async {
final progress = logger.progress('Fetching usage');
final List<AppUsage>? appsUsage;
try {
appsUsage = await codePushClient.getUsage();
progress.complete();
} catch (error) {
progress.fail(error.toString());
exit(ExitCode.software.code);
}
return appsUsage;
}
}
@@ -1,3 +1,4 @@
export 'account_command.dart';
export 'account_usage_command.dart';
export 'create_account_command.dart';
export 'subscribe_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());
}
@@ -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<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 List<AppUsage> {
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();
}
}
@@ -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);
});
});
});
});
}
@@ -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>(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);
});
});
}