chore(shorebird_cli): remove shorebird account downgrade command (#1347)
This commit is contained in:
@@ -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();
|
||||
|
||||
-24
@@ -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;
|
||||
}
|
||||
-31
@@ -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';
|
||||
|
||||
-16
@@ -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()),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
+1
-1
@@ -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});
|
||||
|
||||
Reference in New Issue
Block a user