Skip to content

Commit

Permalink
feat(shorebird_cli): add shorebird collaborators delete (#510)
Browse files Browse the repository at this point in the history
  • Loading branch information
felangel authored May 17, 2023
1 parent 6727a4d commit a5338d5
Show file tree
Hide file tree
Showing 8 changed files with 319 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ ${styleBold.wrap(lightGreen.wrap('🚀 Ready to add a new collaborator!'))}

final progress = logger.progress('Adding collaborator');
try {
await client.createAppCollaborator(appId: appId, email: collaborator);
await client.createCollaborator(appId: appId, email: collaborator);
progress.complete();
} catch (error) {
progress.fail();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export 'add_collaborators_command.dart';
export 'collaborators_command.dart';
export 'delete_collaborators_command.dart';
export 'list_collaborators_command.dart';
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class CollaboratorsCommand extends ShorebirdCommand {
/// {@macro collaborators_command}
CollaboratorsCommand({required super.logger}) {
addSubcommand(AddCollaboratorsCommand(logger: logger));
addSubcommand(DeleteCollaboratorsCommand(logger: logger));
addSubcommand(ListCollaboratorsCommand(logger: logger));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import 'dart:async';

import 'package:collection/collection.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 delete_collaborators_command}
/// `shorebird collaborators delete`
/// Delete an existing collaborator from a Shorebird app.
/// {@endtemplate}
class DeleteCollaboratorsCommand extends ShorebirdCommand
with AuthLoggerMixin, ShorebirdConfigMixin {
/// {@macro delete_collaborators_command}
DeleteCollaboratorsCommand({
required super.logger,
super.buildCodePushClient,
super.auth,
}) {
argParser
..addOption(
_appIdOption,
help: 'The app id that contains the collaborator to be deleted.',
)
..addOption(
_collaboratorEmailOption,
help: 'The email of the collaborator to delete.',
);
}

static const String _appIdOption = 'app-id';
static const String _collaboratorEmailOption = 'email';

@override
String get description =>
'Delete an existing collaborator from a Shorebird app.';

@override
String get name => 'delete';

@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 email = results[_collaboratorEmailOption] as String? ??
logger.prompt(
'''${lightGreen.wrap('?')} What is the email of the collaborator you would like to delete?''',
);

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

final collaborator = collaborators.firstWhereOrNull(
(c) => c.email == email,
);
if (collaborator == null) {
logger.err(
'''
Could not find a collaborator with the email "$email".
Available collaborators:
${collaborators.map((c) => ' - ${c.email}').join('\n')}''',
);
return ExitCode.software.code;
}

logger.info(
'''
${styleBold.wrap(lightGreen.wrap('🗑️ Ready to delete an existing collaborator!'))}
📱 App ID: ${lightCyan.wrap(appId)}
🤝 Collaborator: ${lightCyan.wrap(collaborator.email)}
''',
);

final confirm = logger.confirm('Would you like to continue?');

if (!confirm) {
logger.info('Aborted.');
return ExitCode.success.code;
}

final progress = logger.progress('Deleting collaborator');
try {
await client.deleteCollaborator(
appId: appId,
userId: collaborator.userId,
);
progress.complete();
} catch (error) {
progress.fail();
logger.err('$error');
return ExitCode.software.code;
}

logger.success('\n✅ Collaborator Deleted!');

return ExitCode.success.code;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ void main() {
'when adding a collaborator fails', () async {
const error = 'oops something went wrong';
when(
() => codePushClient.createAppCollaborator(
() => codePushClient.createCollaborator(
appId: any(named: 'appId'),
email: any(named: 'email'),
),
Expand All @@ -109,7 +109,7 @@ void main() {
when(() => argResults['email']).thenReturn(null);
when(() => logger.prompt(any())).thenReturn(email);
when(
() => codePushClient.createAppCollaborator(
() => codePushClient.createCollaborator(
appId: any(named: 'appId'),
email: any(named: 'email'),
),
Expand All @@ -121,13 +121,13 @@ void main() {
),
).called(1);
verify(
() => codePushClient.createAppCollaborator(appId: appId, email: email),
() => codePushClient.createCollaborator(appId: appId, email: email),
).called(1);
});

test('returns ExitCode.success on success', () async {
when(
() => codePushClient.createAppCollaborator(
() => codePushClient.createCollaborator(
appId: any(named: 'appId'),
email: any(named: 'email'),
),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
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 {}

class _MockProgress extends Mock implements Progress {}

void main() {
group('delete', () {
const appId = 'test-app-id';
const email = '[email protected]';
const collaborator = Collaborator(userId: 0, email: email);

late ArgResults argResults;
late http.Client httpClient;
late Auth auth;
late CodePushClient codePushClient;
late Logger logger;
late Progress progress;
late DeleteCollaboratorsCommand command;

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

when(() => argResults['app-id']).thenReturn(appId);
when(() => argResults['email']).thenReturn(email);
when(() => auth.isAuthenticated).thenReturn(true);
when(() => auth.client).thenReturn(httpClient);
when(() => logger.confirm(any())).thenReturn(true);
when(() => logger.progress(any())).thenReturn(progress);
when(
() => codePushClient.getCollaborators(appId: any(named: 'appId')),
).thenAnswer((_) async => [collaborator]);
when(
() => codePushClient.deleteCollaborator(
appId: any(named: 'appId'),
userId: any(named: 'userId'),
),
).thenAnswer((_) async {});
});

test('description is correct', () {
expect(
command.description,
equals('Delete an existing collaborator from a Shorebird app.'),
);
});

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.success when user aborts', () async {
when(() => logger.confirm(any())).thenReturn(false);
expect(await command.run(), ExitCode.success.code);
verifyNever(
() => codePushClient.deleteCollaborator(
appId: any(named: 'appId'),
userId: any(named: 'userId'),
),
);
verify(() => logger.info('Aborted.')).called(1);
});

test(
'returns ExitCode.software '
'when fetching collaborators fails', () async {
const error = 'oops something went wrong';
when(
() => codePushClient.getCollaborators(appId: any(named: 'appId')),
).thenThrow(error);
expect(await command.run(), ExitCode.software.code);
verify(() => logger.err(error)).called(1);
});

test('returns ExitCode.software when collaborator does not exist',
() async {
when(
() => codePushClient.getCollaborators(appId: any(named: 'appId')),
).thenAnswer((_) async => []);
expect(await command.run(), ExitCode.software.code);
verify(
() => logger.err(
any(
that: contains(
'Could not find a collaborator with the email "$email".',
),
),
),
).called(1);
});

test(
'returns ExitCode.software '
'when deleting a collaborator fails', () async {
const error = 'oops something went wrong';
when(
() => codePushClient.deleteCollaborator(
appId: any(named: 'appId'),
userId: any(named: 'userId'),
),
).thenThrow(error);
expect(await command.run(), ExitCode.software.code);
verify(() => logger.err(error)).called(1);
});

test('prompts for email when not provided', () async {
when(() => argResults['email']).thenReturn(null);
when(() => logger.prompt(any())).thenReturn(email);
when(
() => codePushClient.deleteCollaborator(
appId: any(named: 'appId'),
userId: any(named: 'userId'),
),
).thenAnswer((_) async => collaborator);
expect(await command.run(), ExitCode.success.code);
verify(
() => logger.prompt(
'''${lightGreen.wrap('?')} What is the email of the collaborator you would like to delete?''',
),
).called(1);
verify(
() => codePushClient.deleteCollaborator(
appId: appId,
userId: collaborator.userId,
),
).called(1);
});

test('returns ExitCode.success on success', () async {
when(
() => codePushClient.deleteCollaborator(
appId: any(named: 'appId'),
userId: any(named: 'userId'),
),
).thenAnswer((_) async {});
expect(await command.run(), ExitCode.success.code);
verify(() => logger.success('\n✅ Collaborator Deleted!')).called(1);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class CodePushClient {

/// Add a new collaborator to the app.
/// Collaborators can manage the app including its releases and patches.
Future<void> createAppCollaborator({
Future<void> createCollaborator({
required String appId,
required String email,
}) async {
Expand Down Expand Up @@ -216,7 +216,7 @@ class CodePushClient {
}

/// Remove [userId] as a collaborator from [appId].
Future<void> deleteAppCollaborator({
Future<void> deleteCollaborator({
required String appId,
required int userId,
}) async {
Expand Down
Loading

0 comments on commit a5338d5

Please sign in to comment.