chore(shorebird_cli): remove shorebird account downgrade command (#1347)

This commit is contained in:
Felix Angelov
2023-10-03 11:25:59 -05:00
committed by GitHub
parent 8080446634
commit 6845de7fcd
13 changed files with 1 additions and 440 deletions
@@ -56,7 +56,6 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner<int> {
'are building Flutter locally.',
);
addCommand(AccountCommand());
addCommand(AppsCommand());
addCommand(BuildCommand());
addCommand(CacheCommand());
@@ -1,2 +0,0 @@
export 'account_command.dart';
export 'downgrade_account_command.dart';
@@ -1,19 +0,0 @@
import 'package:shorebird_cli/src/command.dart';
import 'package:shorebird_cli/src/commands/commands.dart';
/// {@template account_command}
/// `shorebird account`
/// Manage your Shorebird account.
/// {@endtemplate}
class AccountCommand extends ShorebirdCommand {
/// {@macro account_command}
AccountCommand() {
addSubcommand(DowngradeAccountCommand());
}
@override
String get name => 'account';
@override
String get description => 'Manage your Shorebird account.';
}
@@ -1,82 +0,0 @@
import 'dart:async';
import 'package:intl/intl.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_validator.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
class DowngradeAccountCommand extends ShorebirdCommand {
DowngradeAccountCommand();
@override
String get name => 'downgrade';
@override
String get description => 'Downgrade your Shorebird account.';
@override
Future<int> run() async {
try {
await shorebirdValidator.validatePreconditions(
checkUserIsAuthenticated: true,
);
} on PreconditionFailedException catch (e) {
return e.exitCode.code;
}
final User user;
try {
final currentUser =
await codePushClientWrapper.codePushClient.getCurrentUser();
if (currentUser == null) {
throw Exception('Failed to retrieve user information.');
}
user = currentUser;
} catch (error) {
logger.err(error.toString());
return ExitCode.software.code;
}
if (!user.hasActiveSubscription) {
logger.err('You do not have a "teams" subscription.');
return ExitCode.software.code;
}
final confirm = logger.confirm(
red.wrap(
'''This will downgrade your Shorebird plan to the "hobby" tier. Are you sure?''',
),
);
if (!confirm) {
logger.info('Aborting.');
return ExitCode.success.code;
}
final progress = logger.progress('Downgrading your plan');
final DateTime cancellationDate;
try {
cancellationDate =
await codePushClientWrapper.codePushClient.cancelSubscription();
} catch (error) {
progress.fail('Failed to downgrade plan. Error: $error');
return ExitCode.software.code;
}
final formattedDate = DateFormat.yMMMMd().format(cancellationDate);
progress.complete(
'''
Your plan has been downgraded.
Note: Your current plan will continue until $formattedDate, after which your account will be on the "hobby" tier.
Apps on devices you've built with Shorebird will continue to function normally, but will be subject to the limits of the "hobby" tier.''',
);
return ExitCode.success.code;
}
}
@@ -1,4 +1,3 @@
export 'account/account.dart';
export 'apps/apps.dart';
export 'build/build_command.dart';
export 'cache/cache.dart';
@@ -1,191 +0,0 @@
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:scoped/scoped.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_cli/src/shorebird_validator.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
import 'package:test/test.dart';
import '../../mocks.dart';
void main() {
group(DowngradeAccountCommand, () {
const noSubscriptionUser = User(id: 1, email: 'tester1@shorebird.dev');
const subscriptionUser = User(
id: 2,
email: 'tester2@shorebird.dev',
hasActiveSubscription: true,
);
late CodePushClientWrapper codePushClientWrapper;
late CodePushClient codePushClient;
late Logger logger;
late Progress progress;
late ShorebirdValidator shorebirdValidator;
late DowngradeAccountCommand command;
R runWithOverrides<R>(R Function() body) {
return runScoped(
body,
values: {
codePushClientWrapperRef.overrideWith(() => codePushClientWrapper),
loggerRef.overrideWith(() => logger),
shorebirdValidatorRef.overrideWith(() => shorebirdValidator),
},
);
}
setUp(() {
codePushClientWrapper = MockCodePushClientWrapper();
codePushClient = MockCodePushClient();
logger = MockLogger();
progress = MockProgress();
shorebirdValidator = MockShorebirdValidator();
when(
() => codePushClientWrapper.codePushClient,
).thenReturn(codePushClient);
when(() => logger.progress(any())).thenReturn(progress);
when(
() => shorebirdValidator.validatePreconditions(
checkUserIsAuthenticated: any(named: 'checkUserIsAuthenticated'),
),
).thenAnswer((_) async {});
command = runWithOverrides(DowngradeAccountCommand.new);
});
test('has a description', () {
expect(command.description, isNotEmpty);
});
test('exits when validation fails', () async {
final exception = ValidationFailedException();
when(
() => shorebirdValidator.validatePreconditions(
checkUserIsAuthenticated: any(named: 'checkUserIsAuthenticated'),
),
).thenThrow(exception);
await expectLater(
runWithOverrides(command.run),
completion(equals(exception.exitCode.code)),
);
verify(
() => shorebirdValidator.validatePreconditions(
checkUserIsAuthenticated: true,
),
).called(1);
});
test('prints an error if fetch current user fails', () async {
when(() => codePushClient.getCurrentUser()).thenThrow(
Exception('an error occurred'),
);
final result = await runWithOverrides(command.run);
expect(result, ExitCode.software.code);
verify(
() => logger.err(any(that: contains('an error occurred'))),
).called(1);
});
test('prints an error if fetch current user returns null', () async {
when(() => codePushClient.getCurrentUser()).thenAnswer((_) async => null);
final result = await runWithOverrides(command.run);
expect(result, ExitCode.software.code);
verify(
() => logger.err(
any(that: contains('Failed to retrieve user information')),
),
).called(1);
});
test(
'prints an error if the user does not have an active subscription',
() async {
when(
() => codePushClient.getCurrentUser(),
).thenAnswer((_) async => noSubscriptionUser);
final result = await runWithOverrides(command.run);
expect(result, ExitCode.software.code);
verify(
() => logger.err(
any(that: contains('You do not have a "teams" subscription')),
),
).called(1);
},
);
test('exits successfully if the user opts not to cancel', () async {
when(
() => codePushClient.getCurrentUser(),
).thenAnswer((_) async => subscriptionUser);
when(
() => logger.confirm(any(that: contains('Are you sure?'))),
).thenReturn(false);
final result = await runWithOverrides(command.run);
expect(result, ExitCode.success.code);
verify(() => logger.info('Aborting.')).called(1);
});
test('prints an error if call to cancel subscription fails', () async {
when(
() => codePushClient.getCurrentUser(),
).thenAnswer((_) async => subscriptionUser);
when(
() => logger.confirm(any(that: contains('Are you sure?'))),
).thenReturn(true);
when(() => codePushClient.cancelSubscription()).thenThrow(
Exception('an error occurred'),
);
final result = await runWithOverrides(command.run);
expect(result, ExitCode.software.code);
verify(
() => progress.fail(any(that: contains('an error occurred'))),
).called(1);
});
test('exits successfully on subscription cancellation', () async {
// Fri Apr 14 2023 07:00:00 GMT+0000
const cancellationTimestamp = 1681455600;
when(
() => codePushClient.getCurrentUser(),
).thenAnswer((_) async => subscriptionUser);
when(
() => logger.confirm(any(that: contains('Are you sure?'))),
).thenReturn(true);
when(() => codePushClient.cancelSubscription()).thenAnswer(
(_) async => DateTime.fromMillisecondsSinceEpoch(
cancellationTimestamp * 1000,
),
);
final result = await runWithOverrides(command.run);
expect(result, ExitCode.success.code);
verify(
() => progress.complete(
any(
that: stringContainsInOrder([
'Your plan has been downgraded.',
'''Note: Your current plan will continue until April 14, 2023, after which your account will be on the "hobby" tier.''',
]),
),
),
).called(1);
});
});
}
@@ -498,19 +498,6 @@ class CodePushClient {
}
}
/// Cancels the current user's subscription.
Future<DateTime> cancelSubscription() async {
final response = await _httpClient.delete(Uri.parse('$_v1/subscriptions'));
if (response.statusCode != HttpStatus.ok) {
throw _parseErrorResponse(response.statusCode, response.body);
}
final json = jsonDecode(response.body) as Map<String, dynamic>;
final timestamp = json['expiration_date'] as int;
return DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
}
/// Closes the client.
void close() => _httpClient.close();
@@ -2074,64 +2074,6 @@ void main() {
});
});
group('cancelSubscription', () {
late Uri uri;
setUp(() {
uri = Uri.parse('${codePushClient.hostedUri}/api/v1/subscriptions');
});
test('makes the correct request', () async {
codePushClient.cancelSubscription().ignore();
final request = verify(() => httpClient.send(captureAny()))
.captured
.single as http.BaseRequest;
expect(request.method, equals('DELETE'));
expect(request.url, equals(v1('subscriptions')));
expect(request.hasStandardHeaders, isTrue);
});
test('throws an exception if the http request fails', () {
when(() => httpClient.send(any())).thenAnswer(
(_) async => http.StreamedResponse(
const Stream.empty(),
HttpStatus.badRequest,
),
);
expect(
codePushClient.cancelSubscription(),
throwsA(
isA<CodePushException>().having(
(e) => e.message,
'message',
CodePushClient.unknownErrorMessage,
),
),
);
});
test('completes when request succeeds', () async {
const timestamp = 1681455600;
when(() => httpClient.send(any())).thenAnswer(
(_) async => http.StreamedResponse(
Stream.value(
utf8.encode(json.encode({'expiration_date': 1681455600})),
),
HttpStatus.ok,
),
);
final response = await codePushClient.cancelSubscription();
expect(response.millisecondsSinceEpoch, timestamp * 1000);
final request = verify(() => httpClient.send(captureAny()))
.captured
.single as http.BaseRequest;
expect(request.url, equals(uri));
});
});
group('close', () {
test('closes the underlying client', () {
codePushClient.close();
@@ -1,24 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
part 'cancel_subscription_response.g.dart';
/// {@template cancel_subscription_response}
/// The request body for DELETE /api/v1/subscriptions.
/// {@endtemplate}
@JsonSerializable()
class CancelSubscriptionResponse {
/// {@macro cancel_subscription_response}
CancelSubscriptionResponse({required this.expirationDate});
/// Converts a JSON object to a [CancelSubscriptionResponse].
factory CancelSubscriptionResponse.fromJson(Json json) =>
_$CancelSubscriptionResponseFromJson(json);
/// Converts a [CancelSubscriptionResponse] to a JSON object.
Json toJson() => _$CancelSubscriptionResponseToJson(this);
/// When this subscription will not longer be active.
@TimestampConverter()
final DateTime expirationDate;
}
@@ -1,31 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: implicit_dynamic_parameter, require_trailing_commas, cast_nullable_to_non_nullable, lines_longer_than_80_chars
part of 'cancel_subscription_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CancelSubscriptionResponse _$CancelSubscriptionResponseFromJson(
Map<String, dynamic> json) =>
$checkedCreate(
'CancelSubscriptionResponse',
json,
($checkedConvert) {
final val = CancelSubscriptionResponse(
expirationDate: $checkedConvert('expiration_date',
(v) => const TimestampConverter().fromJson(v as int)),
);
return val;
},
fieldKeyMap: const {'expirationDate': 'expiration_date'},
);
Map<String, dynamic> _$CancelSubscriptionResponseToJson(
CancelSubscriptionResponse instance) =>
<String, dynamic>{
'expiration_date':
const TimestampConverter().toJson(instance.expirationDate),
};
@@ -1,4 +1,3 @@
export 'cancel_subscription/cancel_subscription_response.dart';
export 'check_for_patches/check_for_patches.dart';
export 'create_app/create_app.dart';
export 'create_app_collaborator/create_app_collaborator.dart';
@@ -1,16 +0,0 @@
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
import 'package:test/test.dart';
void main() {
group(CancelSubscriptionResponse, () {
test('can be (de)serialized', () {
final response = CancelSubscriptionResponse(
expirationDate: DateTime.now(),
);
expect(
CancelSubscriptionResponse.fromJson(response.toJson()).toJson(),
equals(response.toJson()),
);
});
});
}
@@ -2,7 +2,7 @@ import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
import 'package:test/test.dart';
void main() {
group(CancelSubscriptionResponse, () {
group(CheckForPatchesRequest, () {
test('can be serialized to json without patch metadata', () {
const response = CheckForPatchesResponse(patchAvailable: true);
expect(response.toJson(), {'patch_available': true, 'patch': null});