Skip to content

Commit

Permalink
feat(shorebird_cli): add shorebird collaborators list (#504)
Browse files Browse the repository at this point in the history
  • Loading branch information
felangel authored May 17, 2023
1 parent 84e2b0f commit d42d3b0
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'list_collaborators_command.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import 'package:shorebird_cli/src/command.dart';
import 'package:shorebird_cli/src/commands/commands.dart';

/// {@template collaborators_command}
/// `shorebird collaborators`
/// Manage collaborators for a Shorebird app.
/// {@endtemplate}
class CollaboratorsCommand extends ShorebirdCommand {
/// {@macro collaborators_command}
CollaboratorsCommand({required super.logger}) {
addSubcommand(ListCollaboratorsCommand(logger: logger));
}

@override
String get description => 'Manage collaborators for a Shorebird app';

@override
String get name => 'collaborators';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import 'dart:async';

import 'package:barbecue/barbecue.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:shorebird_cli/src/auth_logger_mixin.dart';
import 'package:shorebird_cli/src/command.dart';
import 'package:shorebird_cli/src/shorebird_config_mixin.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';

/// {@template list_collaborators_command}
/// `shorebird collaborators list`
/// List all collaborators for a Shorebird app.
/// {@endtemplate}
class ListCollaboratorsCommand extends ShorebirdCommand
with AuthLoggerMixin, ShorebirdConfigMixin {
/// {@macro list_collaborators_command}
ListCollaboratorsCommand({
required super.logger,
super.buildCodePushClient,
super.auth,
}) {
argParser.addOption(
_appIdOption,
help: 'The app id to list collaborators for.',
);
}

static const String _appIdOption = 'app-id';

@override
String get name => 'list';

@override
String get description => 'List all collaborators for a Shorebird app.';

@override
List<String> get aliases => ['ls'];

@override
Future<int>? run() async {
if (!auth.isAuthenticated) {
printNeedsAuthInstructions();
return ExitCode.noUser.code;
}

final client = buildCodePushClient(
httpClient: auth.client,
hostedUri: hostedUri,
);

final appId = results[_appIdOption] as String? ?? getShorebirdYaml()?.appId;
if (appId == null) {
logger.err(
'''
Could not find an app id.
You must either specify an app id via the "--$_appIdOption" flag or run this command from within a directory with a valid "shorebird.yaml" file.''',
);
return ExitCode.usage.code;
}

final List<Collaborator> collaborators;
try {
collaborators = await client.getCollaborators(appId: appId);
} catch (error) {
logger.err('$error');
return ExitCode.software.code;
}

logger.info(
'''
📱 App ID: ${lightCyan.wrap(appId)}
🤝 Collaborators''',
);

if (collaborators.isEmpty) {
logger.info('(empty)');
return ExitCode.success.code;
}

logger.info(collaborators.prettyPrint());

return ExitCode.success.code;
}
}

extension on List<Collaborator> {
String prettyPrint() {
const cellStyle = CellStyle(
paddingLeft: 1,
paddingRight: 1,
borderBottom: true,
borderTop: true,
borderLeft: true,
borderRight: true,
);
return Table(
cellStyle: cellStyle,
header: const TableSection(
rows: [
Row(cells: [Cell('Email')])
],
),
body: TableSection(
rows: [
for (final collaborator in this)
Row(cells: [Cell(collaborator.email)]),
],
),
).render();
}
}
1 change: 1 addition & 0 deletions packages/shorebird_cli/lib/src/commands/commands.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export 'account/account.dart';
export 'apps/apps.dart';
export 'build/build_command.dart';
export 'cache/cache.dart';
export 'collaborators/collaborators.dart';
export 'doctor_command.dart';
export 'init_command.dart';
export 'login_command.dart';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import 'package:args/args.dart';
import 'package:http/http.dart' as http;
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:shorebird_cli/src/auth/auth.dart';
import 'package:shorebird_cli/src/commands/commands.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
import 'package:test/test.dart';

class _MockArgResults extends Mock implements ArgResults {}

class _MockHttpClient extends Mock implements http.Client {}

class _MockAuth extends Mock implements Auth {}

class _MockCodePushClient extends Mock implements CodePushClient {}

class _MockLogger extends Mock implements Logger {}

void main() {
group('collborators list', () {
const appId = 'test-app-id';

late ArgResults argResults;
late http.Client httpClient;
late Auth auth;
late CodePushClient codePushClient;
late Logger logger;
late ListCollaboratorsCommand command;

setUp(() {
argResults = _MockArgResults();
httpClient = _MockHttpClient();
auth = _MockAuth();
codePushClient = _MockCodePushClient();
logger = _MockLogger();
command = ListCollaboratorsCommand(
auth: auth,
buildCodePushClient: ({
required http.Client httpClient,
Uri? hostedUri,
}) {
return codePushClient;
},
logger: logger,
)..testArgResults = argResults;

when(() => argResults['app-id']).thenReturn(appId);
when(() => auth.isAuthenticated).thenReturn(true);
when(() => auth.client).thenReturn(httpClient);
});

test('name is correct', () {
expect(command.name, equals('list'));
});

test('description is correct', () {
expect(
command.description,
equals('List all collaborators for a Shorebird app.'),
);
});

test('alias is correct', () {
expect(command.aliases, equals(['ls']));
});

test('returns ExitCode.noUser when not logged in', () async {
when(() => auth.isAuthenticated).thenReturn(false);
expect(await command.run(), ExitCode.noUser.code);
});

test('returns ExitCode.usage when app id is missing.', () async {
when(() => argResults['app-id']).thenReturn(null);
expect(await command.run(), ExitCode.usage.code);
});

test('returns ExitCode.software when unable to get collaborators',
() async {
when(
() => codePushClient.getCollaborators(appId: any(named: 'appId')),
).thenThrow(Exception());
expect(await command.run(), ExitCode.software.code);
});

test('returns ExitCode.success when collaborators are empty', () async {
when(
() => codePushClient.getCollaborators(appId: any(named: 'appId')),
).thenAnswer((_) async => []);
expect(await command.run(), ExitCode.success.code);
verify(() => logger.info('(empty)')).called(1);
});

test('returns ExitCode.success when collaborators are not empty', () async {
final collaborators = [
const Collaborator(userId: 0, email: '[email protected]'),
const Collaborator(userId: 1, email: '[email protected]'),
];
when(
() => codePushClient.getCollaborators(appId: any(named: 'appId')),
).thenAnswer((_) async => collaborators);
expect(await command.run(), ExitCode.success.code);
verify(
() => logger.info(
'''
📱 App ID: ${lightCyan.wrap(appId)}
🤝 Collaborators''',
),
).called(1);
verify(
() => logger.info(
'''
┌────────────────────────┐
│ Email │
├────────────────────────┤
[email protected]
├────────────────────────┤
[email protected]
└────────────────────────┘''',
),
).called(1);
});
});
}

0 comments on commit d42d3b0

Please sign in to comment.