feat(shorebird_cli): add shorebird collaborators delete (#510)
This commit is contained in:
+1
-1
@@ -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();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export 'add_collaborators_command.dart';
|
||||
export 'collaborators_command.dart';
|
||||
export 'delete_collaborators_command.dart';
|
||||
export 'list_collaborators_command.dart';
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
+126
@@ -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;
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -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'),
|
||||
),
|
||||
@@ -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'),
|
||||
),
|
||||
@@ -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'),
|
||||
),
|
||||
|
||||
+176
@@ -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 = 'jane.doe@shorebird.dev';
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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 {
|
||||
|
||||
@@ -53,7 +53,7 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('createAppCollaborator', () {
|
||||
group('createCollaborator', () {
|
||||
const appId = 'test-app-id';
|
||||
const email = 'jane.doe@shorebird.dev';
|
||||
|
||||
@@ -69,7 +69,7 @@ void main() {
|
||||
);
|
||||
|
||||
expect(
|
||||
codePushClient.createAppCollaborator(appId: appId, email: email),
|
||||
codePushClient.createCollaborator(appId: appId, email: email),
|
||||
throwsA(
|
||||
isA<CodePushException>().having(
|
||||
(e) => e.message,
|
||||
@@ -95,7 +95,7 @@ void main() {
|
||||
);
|
||||
|
||||
expect(
|
||||
codePushClient.createAppCollaborator(appId: appId, email: email),
|
||||
codePushClient.createCollaborator(appId: appId, email: email),
|
||||
throwsA(
|
||||
isA<CodePushException>().having(
|
||||
(e) => e.message,
|
||||
@@ -115,7 +115,7 @@ void main() {
|
||||
),
|
||||
).thenAnswer((_) async => http.Response('', HttpStatus.created));
|
||||
|
||||
await codePushClient.createAppCollaborator(appId: appId, email: email);
|
||||
await codePushClient.createCollaborator(appId: appId, email: email);
|
||||
|
||||
final uri = verify(
|
||||
() => httpClient.post(
|
||||
@@ -868,7 +868,7 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('deleteAppCollaborator', () {
|
||||
group('deleteCollaborator', () {
|
||||
const appId = 'test-app-id';
|
||||
const userId = 42;
|
||||
|
||||
@@ -880,7 +880,7 @@ void main() {
|
||||
);
|
||||
|
||||
expect(
|
||||
codePushClient.deleteAppCollaborator(appId: appId, userId: userId),
|
||||
codePushClient.deleteCollaborator(appId: appId, userId: userId),
|
||||
throwsA(
|
||||
isA<CodePushException>().having(
|
||||
(e) => e.message,
|
||||
@@ -902,7 +902,7 @@ void main() {
|
||||
);
|
||||
|
||||
expect(
|
||||
codePushClient.deleteAppCollaborator(appId: appId, userId: userId),
|
||||
codePushClient.deleteCollaborator(appId: appId, userId: userId),
|
||||
throwsA(
|
||||
isA<CodePushException>().having(
|
||||
(e) => e.message,
|
||||
@@ -921,7 +921,7 @@ void main() {
|
||||
),
|
||||
).thenAnswer((_) async => http.Response('', HttpStatus.noContent));
|
||||
|
||||
await codePushClient.deleteAppCollaborator(
|
||||
await codePushClient.deleteCollaborator(
|
||||
appId: appId,
|
||||
userId: userId,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user