feat(shorebird_cli): shorebird apps create (#75)

This commit is contained in:
Felix Angelov
2023-03-15 12:42:03 -05:00
committed by GitHub
parent ea132796f5
commit ab3a7a8976
16 changed files with 207 additions and 54 deletions
+20
View File
@@ -74,6 +74,26 @@ shorebird logout
✓ Logging out of shorebird.dev (1ms)
```
### Create App
To create an app use the `shorebird apps create` command. An app-id can be specified as a CLI option but shorebird will default to the product_id defined in the `shorebird.yaml`
```bash
# Create an app using default app id
shorebird apps create
# Create an app using an explicit app id
shorebird apps create --app-id "my-app-id"
```
**Sample**
```
shorebird apps create
? Enter the App ID (default-id) my-app-id
Created new app: my-app-id
```
### List Apps
List all existing apps in Shorebird using the `shorebird apps list` command:
@@ -13,8 +13,8 @@ class Auth {
static const _applicationName = 'shorebird';
static const _sessionFileName = 'shorebird-session.json';
void login({required String projectId, required String apiKey}) {
_session = Session(projectId: projectId, apiKey: apiKey);
void login({required String apiKey}) {
_session = Session(apiKey: apiKey);
_flushSession(_session!);
}
@@ -1,15 +1,13 @@
class Session {
const Session({required this.projectId, required this.apiKey});
const Session({required this.apiKey});
factory Session.fromJson(Map<String, dynamic> json) {
return Session(
projectId: json['project_id'] as String,
apiKey: json['api_key'] as String,
);
}
final String projectId;
final String apiKey;
Map<String, dynamic> toJson() => {'project_id': projectId, 'api_key': apiKey};
Map<String, dynamic> toJson() => {'api_key': apiKey};
}
@@ -1,2 +1,3 @@
export 'apps_command.dart';
export 'create_apps_command.dart';
export 'list_apps_command.dart';
@@ -1,5 +1,5 @@
import 'package:shorebird_cli/src/command.dart';
import 'package:shorebird_cli/src/commands/apps/list_apps_command.dart';
import 'package:shorebird_cli/src/commands/commands.dart';
/// {@template apps_command}
///
@@ -9,6 +9,7 @@ import 'package:shorebird_cli/src/commands/apps/list_apps_command.dart';
class AppsCommand extends ShorebirdCommand {
/// {@macro apps_command}
AppsCommand({required super.logger}) {
addSubcommand(CreateAppCommand(logger: logger));
addSubcommand(ListAppsCommand(logger: logger));
}
@@ -0,0 +1,73 @@
import 'dart:async';
import 'package:mason_logger/mason_logger.dart';
import 'package:shorebird_cli/src/command.dart';
import 'package:shorebird_cli/src/shorebird_config_mixin.dart';
/// {@template create_app_command}
///
/// `shorebird apps create`
/// Create a new app on Shorebird.
/// {@endtemplate}
class CreateAppCommand extends ShorebirdCommand with ShorebirdConfigMixin {
/// {@macro create_app_command}
CreateAppCommand({
required super.logger,
super.buildCodePushClient,
super.auth,
}) {
argParser.addOption(
'app-id',
help: '''
The unique application identifier.
Defaults to the product_id in "shorebird.yaml".''',
);
}
@override
String get description => 'Create a new app on Shorebird.';
@override
String get name => 'create';
@override
Future<int>? run() async {
final session = auth.currentSession;
if (session == null) {
logger.err('You must be logged in.');
return ExitCode.noUser.code;
}
final appId = results['app-id'] as String?;
late final String productId;
if (appId == null) {
String? defaultProductId;
try {
defaultProductId = getShorebirdYaml()?.productId;
} catch (_) {}
productId = logger.prompt(
'${lightGreen.wrap('?')} Enter the App ID',
defaultValue: defaultProductId,
);
} else {
productId = appId;
}
final client = buildCodePushClient(apiKey: session.apiKey);
try {
await client.createApp(productId: productId);
} catch (error) {
logger.err('Unable to create app\n$error');
return ExitCode.software.code;
}
logger.info(
'${lightGreen.wrap('Created new app: ${cyan.wrap(productId)}')}',
);
return ExitCode.success.code;
}
}
@@ -72,9 +72,10 @@ ${lightGreen.wrap('🐦 Shorebird initialized successfully!')}
Reference the following commands to get started:
✨ To create a new app use: "${lightCyan.wrap('shorebird apps create')}".
🚙 To run your project use: "${lightCyan.wrap('shorebird run')}".
📦 To build your project use: "${lightCyan.wrap('shorebird build')}".
🚀 To publish a new update use: "${lightCyan.wrap('shorebird publish')}".
🚀 To publish an update use: "${lightCyan.wrap('shorebird publish')}".
For more information about Shorebird, visit ${link(uri: Uri.parse('https://shorebird.dev'))}''',
);
@@ -31,7 +31,7 @@ class LoginCommand extends ShorebirdCommand {
);
final loginProgress = logger.progress('Logging into shorebird.dev');
try {
auth.login(projectId: 'example', apiKey: apiKey);
auth.login(apiKey: apiKey);
loginProgress.complete();
logger.success('You are now logged in.');
return ExitCode.success.code;
@@ -5,7 +5,6 @@ import 'package:test/test.dart';
void main() {
group('Auth', () {
const apiKey = 'test-api-key';
const projectId = 'test-project-id';
late Auth auth;
@@ -15,30 +14,24 @@ void main() {
group('login', () {
test('should set the current session', () {
auth.login(apiKey: apiKey, projectId: projectId);
auth.login(apiKey: apiKey);
expect(
auth.currentSession,
isA<Session>()
.having((s) => s.apiKey, 'apiKey', apiKey)
.having((s) => s.projectId, 'projectId', projectId),
isA<Session>().having((s) => s.apiKey, 'apiKey', apiKey),
);
expect(
Auth().currentSession,
isA<Session>()
.having((s) => s.apiKey, 'apiKey', apiKey)
.having((s) => s.projectId, 'projectId', projectId),
isA<Session>().having((s) => s.apiKey, 'apiKey', apiKey),
);
});
});
group('logout', () {
test('clears session and wipes state', () {
auth.login(apiKey: apiKey, projectId: projectId);
auth.login(apiKey: apiKey);
expect(
auth.currentSession,
isA<Session>()
.having((s) => s.apiKey, 'apiKey', apiKey)
.having((s) => s.projectId, 'projectId', projectId),
isA<Session>().having((s) => s.apiKey, 'apiKey', apiKey),
);
auth.logout();
@@ -0,0 +1,89 @@
import 'package:args/args.dart';
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/auth/session.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 _MockAuth extends Mock implements Auth {}
class _MockCodePushClient extends Mock implements CodePushClient {}
class _MockLogger extends Mock implements Logger {}
void main() {
group('create', () {
const apiKey = 'test-api-key';
const productId = 'example';
const session = Session(apiKey: apiKey);
late ArgResults argResults;
late Auth auth;
late Logger logger;
late CodePushClient codePushClient;
late CreateAppCommand command;
setUp(() {
argResults = _MockArgResults();
auth = _MockAuth();
logger = _MockLogger();
codePushClient = _MockCodePushClient();
command = CreateAppCommand(
auth: auth,
buildCodePushClient: ({required String apiKey}) => codePushClient,
logger: logger,
)..testArgResults = argResults;
when(() => auth.currentSession).thenReturn(session);
});
test('returns correct description', () {
expect(command.description, equals('Create a new app on Shorebird.'));
});
test('returns no user error when not logged in', () async {
when(() => auth.currentSession).thenReturn(null);
final result = await command.run();
expect(result, ExitCode.noUser.code);
});
test('prompts for app-id when not provided', () async {
when(() => logger.prompt(any())).thenReturn(productId);
await command.run();
verify(() => logger.prompt(any())).called(1);
verify(() => codePushClient.createApp(productId: productId)).called(1);
});
test('uses provided app-id when provided', () async {
when(() => argResults['app-id']).thenReturn(productId);
await command.run();
verifyNever(() => logger.prompt(any()));
verify(() => codePushClient.createApp(productId: productId)).called(1);
});
test('returns success when app is created', () async {
when(() => argResults['app-id']).thenReturn(productId);
when(
() => codePushClient.createApp(productId: productId),
).thenAnswer((_) async {});
final result = await command.run();
expect(result, ExitCode.success.code);
});
test('returns software error when app creation fails', () async {
when(() => argResults['app-id']).thenReturn(productId);
when(
() => codePushClient.createApp(productId: productId),
).thenThrow(Exception());
final result = await command.run();
expect(result, ExitCode.software.code);
verify(
() => logger.err(any(that: contains('Unable to create app'))),
).called(1);
});
});
}
@@ -14,10 +14,7 @@ class _MockLogger extends Mock implements Logger {}
void main() {
group('list', () {
const session = Session(
projectId: 'test-project-id',
apiKey: 'test-api-key',
);
const session = Session(apiKey: 'test-api-key');
late Auth auth;
late CodePushClient codePushClient;
@@ -24,10 +24,7 @@ class _MockCodePushClient extends Mock implements CodePushClient {}
void main() {
group('build', () {
const session = Session(
apiKey: 'test-api-key',
projectId: 'test-project-id',
);
const session = Session(apiKey: 'test-api-key');
late ArgResults argResults;
late Auth auth;
@@ -14,8 +14,7 @@ class _MockProgress extends Mock implements Progress {}
void main() {
group('login', () {
const apiKey = 'test-api-key';
const projectId = 'example';
const session = Session(apiKey: apiKey, projectId: projectId);
const session = Session(apiKey: apiKey);
late Logger logger;
late Auth auth;
@@ -46,19 +45,14 @@ void main() {
when(() => logger.prompt(any())).thenReturn(apiKey);
when(() => auth.currentSession).thenReturn(null);
when(
() => auth.login(
apiKey: any(named: 'apiKey'),
projectId: any(named: 'projectId'),
),
() => auth.login(apiKey: any(named: 'apiKey')),
).thenThrow(error);
final result = await loginCommand.run();
expect(result, equals(ExitCode.software.code));
verify(() => logger.progress('Logging into shorebird.dev')).called(1);
verify(
() => auth.login(apiKey: apiKey, projectId: projectId),
).called(1);
verify(() => auth.login(apiKey: apiKey)).called(1);
verify(() => logger.err(error.toString())).called(1);
});
@@ -66,19 +60,14 @@ void main() {
when(() => logger.prompt(any())).thenReturn(apiKey);
when(() => auth.currentSession).thenReturn(null);
when(
() => auth.login(
apiKey: any(named: 'apiKey'),
projectId: any(named: 'projectId'),
),
() => auth.login(apiKey: any(named: 'apiKey')),
).thenAnswer((_) async {});
final result = await loginCommand.run();
expect(result, equals(ExitCode.success.code));
verify(() => logger.progress('Logging into shorebird.dev')).called(1);
verify(
() => auth.login(apiKey: apiKey, projectId: projectId),
).called(1);
verify(() => auth.login(apiKey: apiKey)).called(1);
verify(
() => logger.success('You are now logged in.'),
).called(1);
@@ -35,7 +35,7 @@ void main() {
});
test('exits with code 0 when logged out successfully', () async {
const session = Session(apiKey: 'test-api-key', projectId: 'example');
const session = Session(apiKey: 'test-api-key');
when(() => auth.currentSession).thenReturn(session);
final progress = _MockProgress();
@@ -28,10 +28,7 @@ class _FakeCommandRunner extends Fake implements CommandRunner<int> {
void main() {
group('publish', () {
const session = Session(
projectId: 'test-project-id',
apiKey: 'test-api-key',
);
const session = Session(apiKey: 'test-api-key');
const productId = 'test-product-id';
const version = '1.2.3';
const pubspecYamlContent = '''
@@ -28,10 +28,7 @@ class _MockCodePushClient extends Mock implements CodePushClient {}
void main() {
group('run', () {
const session = Session(
apiKey: 'test-api-key',
projectId: 'test-project-id',
);
const session = Session(apiKey: 'test-api-key');
late ArgResults argResults;
late Auth auth;