feat(shorebird_cli): shorebird channels delete (#496)

This commit is contained in:
Felix Angelov
2023-05-15 14:34:08 -05:00
committed by GitHub
parent 02a74e2a37
commit 78cd61a686
4 changed files with 282 additions and 0 deletions
@@ -1,3 +1,4 @@
export 'channels_command.dart';
export 'create_channels_command.dart';
export 'delete_channels_command.dart';
export 'list_channels_command.dart';
@@ -9,6 +9,7 @@ class ChannelsCommand extends ShorebirdCommand {
/// {@macro channels_command}
ChannelsCommand({required super.logger}) {
addSubcommand(CreateChannelsCommand(logger: logger));
addSubcommand(DeleteChannelsCommand(logger: logger));
addSubcommand(ListChannelsCommand(logger: logger));
}
@@ -0,0 +1,125 @@
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_channels_command}
/// `shorebird channels delete`
/// Delete an existing channel for a Shorebird app.
/// {@endtemplate}
class DeleteChannelsCommand extends ShorebirdCommand
with AuthLoggerMixin, ShorebirdConfigMixin {
/// {@macro delete_channels_command}
DeleteChannelsCommand({
required super.logger,
super.buildCodePushClient,
super.auth,
}) {
argParser
..addOption(
_appIdOption,
help: 'The app id that contains the channel to be deleted.',
)
..addOption(
_channelNameOption,
help: 'The name of the channel to delete.',
);
}
static const String _appIdOption = 'app-id';
static const String _channelNameOption = 'name';
@override
String get description => 'Delete an existing channel for 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 channelName = results[_channelNameOption] as String? ??
logger.prompt(
'''${lightGreen.wrap('?')} What is the name of the channel you would like to delete?''',
);
;
final getChannelsProgress = logger.progress('Fetching channels');
final List<Channel> channels;
try {
channels = await client.getChannels(appId: appId);
getChannelsProgress.complete();
} catch (error) {
getChannelsProgress.fail();
logger.err('$error');
return ExitCode.software.code;
}
final channel = channels.firstWhereOrNull((c) => c.name == channelName);
if (channel == null) {
logger.err(
'''
Could not find a channel with the name "$channelName".
Available channels:
${channels.map((c) => ' - ${c.name}').join('\n')}''',
);
return ExitCode.software.code;
}
logger.info(
'''
${styleBold.wrap(lightGreen.wrap('🗑️ Ready to delete an existing channel!'))}
📱 App ID: ${lightCyan.wrap(appId)}
📺 Channel: ${lightCyan.wrap(channel.name)}
''',
);
final confirm = logger.confirm('Would you like to continue?');
if (!confirm) {
logger.info('Aborted.');
return ExitCode.success.code;
}
final progress = logger.progress('Deleting channel');
try {
await client.deleteChannel(channelId: channel.id);
progress.complete();
} catch (error) {
progress.fail();
logger.err('$error');
return ExitCode.software.code;
}
logger.success('\n✅ Channel Deleted!');
return ExitCode.success.code;
}
}
@@ -0,0 +1,155 @@
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 channelName = 'my-channel';
const channel = Channel(id: 0, appId: appId, name: channelName);
late ArgResults argResults;
late http.Client httpClient;
late Auth auth;
late CodePushClient codePushClient;
late Logger logger;
late Progress progress;
late DeleteChannelsCommand command;
setUp(() {
argResults = _MockArgResults();
httpClient = _MockHttpClient();
auth = _MockAuth();
codePushClient = _MockCodePushClient();
logger = _MockLogger();
progress = _MockProgress();
command = DeleteChannelsCommand(
auth: auth,
buildCodePushClient: ({
required http.Client httpClient,
Uri? hostedUri,
}) {
return codePushClient;
},
logger: logger,
)..testArgResults = argResults;
when(() => argResults['app-id']).thenReturn(appId);
when(() => argResults['name']).thenReturn(channelName);
when(() => auth.isAuthenticated).thenReturn(true);
when(() => auth.client).thenReturn(httpClient);
when(() => logger.confirm(any())).thenReturn(true);
when(() => logger.progress(any())).thenReturn(progress);
when(
() => codePushClient.getChannels(appId: any(named: 'appId')),
).thenAnswer((_) async => [channel]);
when(
() => codePushClient.deleteChannel(channelId: any(named: 'channelId')),
).thenAnswer((_) async {});
});
test('description is correct', () {
expect(
command.description,
equals('Delete an existing channel for 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.deleteChannel(
channelId: any(named: 'channelId'),
),
);
verify(() => logger.info('Aborted.')).called(1);
});
test('returns ExitCode.software when fetching channels fails', () async {
const error = 'oops something went wrong';
when(
() => codePushClient.getChannels(appId: any(named: 'appId')),
).thenThrow(error);
expect(await command.run(), ExitCode.software.code);
verify(() => logger.err(error)).called(1);
});
test('returns ExitCode.software when channel does not exist', () async {
when(
() => codePushClient.getChannels(appId: any(named: 'appId')),
).thenAnswer((_) async => []);
expect(await command.run(), ExitCode.software.code);
verify(
() => logger.err(
any(
that: contains(
'Could not find a channel with the name "$channelName".',
),
),
),
).called(1);
});
test('returns ExitCode.software when deleting a channel fails', () async {
const error = 'oops something went wrong';
when(
() => codePushClient.deleteChannel(channelId: any(named: 'channelId')),
).thenThrow(error);
expect(await command.run(), ExitCode.software.code);
verify(() => logger.err(error)).called(1);
});
test('prompts for channel name when not provided', () async {
when(() => argResults['name']).thenReturn(null);
when(() => logger.prompt(any())).thenReturn(channelName);
when(
() => codePushClient.deleteChannel(channelId: any(named: 'channelId')),
).thenAnswer((_) async => channel);
expect(await command.run(), ExitCode.success.code);
verify(
() => logger.prompt(
'''${lightGreen.wrap('?')} What is the name of the channel you would like to delete?''',
),
).called(1);
verify(
() => codePushClient.deleteChannel(channelId: channel.id),
).called(1);
});
test('returns ExitCode.success on success', () async {
when(
() => codePushClient.deleteChannel(channelId: any(named: 'channelId')),
).thenAnswer((_) async => channel);
expect(await command.run(), ExitCode.success.code);
verify(() => logger.success('\n✅ Channel Deleted!')).called(1);
});
});
}