diff --git a/packages/shorebird_cli/lib/src/auth/auth.dart b/packages/shorebird_cli/lib/src/auth/auth.dart index a725e0c4a..6c3cf7240 100644 --- a/packages/shorebird_cli/lib/src/auth/auth.dart +++ b/packages/shorebird_cli/lib/src/auth/auth.dart @@ -1,54 +1,111 @@ import 'dart:convert'; import 'dart:io'; +import 'package:googleapis_auth/auth_io.dart'; +import 'package:http/http.dart' as http; import 'package:path/path.dart' as p; -import 'package:shorebird_cli/src/auth/session.dart'; import 'package:shorebird_cli/src/config/config.dart'; +export 'package:googleapis_auth/googleapis_auth.dart' show AccessCredentials; + +final _clientId = ClientId( + /// Shorebird CLI's OAuth 2.0 identifier. + '523302233293-eia5antm0tgvek240t46orctktiabrek.apps.googleusercontent.com', + + /// Shorebird CLI's OAuth 2.0 secret. + /// + /// This isn't actually meant to be kept secret. + /// There is no way to properly secure a secret for installed/console applications. + /// Fortunately the OAuth2 flow used in this case assumes that the app cannot + /// keep secrets so this particular secret DOES NOT need to be kept secret. + /// You should however make sure not to re-use the same secret + /// anywhere secrecy is required. + /// + /// For more info see: https://developers.google.com/identity/protocols/oauth2/native-app + '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', +); +final _scopes = ['openid', 'https://www.googleapis.com/auth/userinfo.email']; + +typedef ObtainAccessCredentials = Future Function( + ClientId clientId, + List scopes, + http.Client client, + void Function(String) userPrompt, +); + class Auth { - Auth() { - _loadSession(); + Auth({ + http.Client? httpClient, + ObtainAccessCredentials? obtainAccessCredentials, + }) : _httpClient = httpClient ?? http.Client(), + _obtainAccessCredentials = + obtainAccessCredentials ?? obtainAccessCredentialsViaUserConsent { + _loadCredentials(); } - static const _sessionFileName = 'shorebird-session.json'; - final sessionFilePath = p.join(shorebirdConfigDir, _sessionFileName); + static const _credentialsFileName = 'credentials.json'; + + final http.Client _httpClient; + final ObtainAccessCredentials _obtainAccessCredentials; + final credentialsFilePath = p.join(shorebirdConfigDir, _credentialsFileName); - void login({required String apiKey}) { - _session = Session(apiKey: apiKey); - _flushSession(_session!); + http.Client get client { + if (credentials == null) return _httpClient; + return autoRefreshingClient(_clientId, credentials!, _httpClient); } - void logout() => _clearSession(); + Future login(void Function(String) prompt) async { + if (credentials != null) return; - Session? _session; + final client = http.Client(); + try { + _credentials = await _obtainAccessCredentials( + _clientId, + _scopes, + client, + prompt, + ); + _flushCredentials(_credentials!); + } finally { + client.close(); + } + } + + void logout() => _clearCredentials(); + + AccessCredentials? _credentials; - Session? get currentSession => _session; + AccessCredentials? get credentials => _credentials; - void _loadSession() { - final sessionFile = File(sessionFilePath); + void _loadCredentials() { + final credentialsFile = File(credentialsFilePath); - if (sessionFile.existsSync()) { + if (credentialsFile.existsSync()) { try { - final contents = sessionFile.readAsStringSync(); - _session = Session.fromJson( + final contents = credentialsFile.readAsStringSync(); + _credentials = AccessCredentials.fromJson( json.decode(contents) as Map, ); } catch (_) {} } } - void _flushSession(Session session) { - File(sessionFilePath) + void _flushCredentials(AccessCredentials credentials) { + File(credentialsFilePath) ..createSync(recursive: true) - ..writeAsStringSync(json.encode(session.toJson())); + ..writeAsStringSync(json.encode(credentials.toJson())); } - void _clearSession() { - _session = null; + void _clearCredentials() { + _credentials = null; - final sessionFile = File(sessionFilePath); - if (sessionFile.existsSync()) { - sessionFile.deleteSync(recursive: true); + final credentialsFile = File(credentialsFilePath); + if (credentialsFile.existsSync()) { + credentialsFile.deleteSync(recursive: true); } } + + void close() { + _httpClient.close(); + } } diff --git a/packages/shorebird_cli/lib/src/auth/session.dart b/packages/shorebird_cli/lib/src/auth/session.dart deleted file mode 100644 index 433f23578..000000000 --- a/packages/shorebird_cli/lib/src/auth/session.dart +++ /dev/null @@ -1,13 +0,0 @@ -class Session { - const Session({required this.apiKey}); - - factory Session.fromJson(Map json) { - return Session( - apiKey: json['api_key'] as String, - ); - } - - final String apiKey; - - Map toJson() => {'api_key': apiKey}; -} diff --git a/packages/shorebird_cli/lib/src/command.dart b/packages/shorebird_cli/lib/src/command.dart index 1c338f6df..f396e33d2 100644 --- a/packages/shorebird_cli/lib/src/command.dart +++ b/packages/shorebird_cli/lib/src/command.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:args/args.dart'; import 'package:args/command_runner.dart'; +import 'package:http/http.dart' as http; import 'package:mason_logger/mason_logger.dart'; import 'package:meta/meta.dart'; import 'package:shorebird_cli/src/auth/auth.dart'; @@ -12,7 +13,7 @@ import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; typedef HashFunction = String Function(List bytes); typedef CodePushClientBuilder = CodePushClient Function({ - required String apiKey, + required http.Client httpClient, Uri? hostedUri, }); diff --git a/packages/shorebird_cli/lib/src/commands/apps/create_apps_command.dart b/packages/shorebird_cli/lib/src/commands/apps/create_apps_command.dart index e06f8fcfd..905243090 100644 --- a/packages/shorebird_cli/lib/src/commands/apps/create_apps_command.dart +++ b/packages/shorebird_cli/lib/src/commands/apps/create_apps_command.dart @@ -35,8 +35,7 @@ Defaults to the name in "pubspec.yaml".''', @override Future? run() async { - final session = auth.currentSession; - if (session == null) { + if (auth.credentials == null) { logger.err('You must be logged in.'); return ExitCode.noUser.code; } diff --git a/packages/shorebird_cli/lib/src/commands/apps/delete_apps_command.dart b/packages/shorebird_cli/lib/src/commands/apps/delete_apps_command.dart index 714a9c0cc..b85413cbf 100644 --- a/packages/shorebird_cli/lib/src/commands/apps/delete_apps_command.dart +++ b/packages/shorebird_cli/lib/src/commands/apps/delete_apps_command.dart @@ -32,8 +32,7 @@ Defaults to the app_id in "shorebird.yaml".''', @override Future? run() async { - final session = auth.currentSession; - if (session == null) { + if (auth.credentials == null) { logger.err('You must be logged in.'); return ExitCode.noUser.code; } @@ -56,7 +55,7 @@ Defaults to the app_id in "shorebird.yaml".''', } final client = buildCodePushClient( - apiKey: session.apiKey, + httpClient: auth.client, hostedUri: hostedUri, ); diff --git a/packages/shorebird_cli/lib/src/commands/apps/list_apps_command.dart b/packages/shorebird_cli/lib/src/commands/apps/list_apps_command.dart index fb64faa30..31fb54031 100644 --- a/packages/shorebird_cli/lib/src/commands/apps/list_apps_command.dart +++ b/packages/shorebird_cli/lib/src/commands/apps/list_apps_command.dart @@ -30,14 +30,13 @@ class ListAppsCommand extends ShorebirdCommand with ShorebirdConfigMixin { @override Future? run() async { - final session = auth.currentSession; - if (session == null) { + if (auth.credentials == null) { logger.err('You must be logged in.'); return ExitCode.noUser.code; } final client = buildCodePushClient( - apiKey: session.apiKey, + httpClient: auth.client, hostedUri: hostedUri, ); diff --git a/packages/shorebird_cli/lib/src/commands/build_command.dart b/packages/shorebird_cli/lib/src/commands/build_command.dart index ea6617128..214a3b78a 100644 --- a/packages/shorebird_cli/lib/src/commands/build_command.dart +++ b/packages/shorebird_cli/lib/src/commands/build_command.dart @@ -35,7 +35,7 @@ class BuildCommand extends ShorebirdCommand @override Future run() async { - if (auth.currentSession == null) { + if (auth.credentials == null) { logger ..err('You must be logged in to build.') ..err("Run 'shorebird login' to log in and try again."); diff --git a/packages/shorebird_cli/lib/src/commands/channels/create_channels_command.dart b/packages/shorebird_cli/lib/src/commands/channels/create_channels_command.dart index 1fda1dec2..7a31f392b 100644 --- a/packages/shorebird_cli/lib/src/commands/channels/create_channels_command.dart +++ b/packages/shorebird_cli/lib/src/commands/channels/create_channels_command.dart @@ -37,14 +37,13 @@ class CreateChannelsCommand extends ShorebirdCommand with ShorebirdConfigMixin { @override Future? run() async { - final session = auth.currentSession; - if (session == null) { + if (auth.credentials == null) { logger.err('You must be logged in to view channels.'); return ExitCode.noUser.code; } final client = buildCodePushClient( - apiKey: session.apiKey, + httpClient: auth.client, hostedUri: hostedUri, ); diff --git a/packages/shorebird_cli/lib/src/commands/channels/list_channels_command.dart b/packages/shorebird_cli/lib/src/commands/channels/list_channels_command.dart index 2d5b727a1..b7f679fa1 100644 --- a/packages/shorebird_cli/lib/src/commands/channels/list_channels_command.dart +++ b/packages/shorebird_cli/lib/src/commands/channels/list_channels_command.dart @@ -36,14 +36,13 @@ class ListChannelsCommand extends ShorebirdCommand with ShorebirdConfigMixin { @override Future? run() async { - final session = auth.currentSession; - if (session == null) { + if (auth.credentials == null) { logger.err('You must be logged in to view channels.'); return ExitCode.noUser.code; } final client = buildCodePushClient( - apiKey: session.apiKey, + httpClient: auth.client, hostedUri: hostedUri, ); diff --git a/packages/shorebird_cli/lib/src/commands/init_command.dart b/packages/shorebird_cli/lib/src/commands/init_command.dart index 49f38c7f3..89be99b92 100644 --- a/packages/shorebird_cli/lib/src/commands/init_command.dart +++ b/packages/shorebird_cli/lib/src/commands/init_command.dart @@ -24,8 +24,7 @@ class InitCommand extends ShorebirdCommand @override Future run() async { - final session = auth.currentSession; - if (session == null) { + if (auth.credentials == null) { logger.err('You must be logged in.'); return ExitCode.noUser.code; } @@ -56,7 +55,7 @@ Please make sure you are running "shorebird init" from the root of your Flutter if (shorebirdYaml != null) { final codePushClient = buildCodePushClient( - apiKey: session.apiKey, + httpClient: auth.client, hostedUri: hostedUri, ); diff --git a/packages/shorebird_cli/lib/src/commands/login_command.dart b/packages/shorebird_cli/lib/src/commands/login_command.dart index 7d1611b3d..6eb06f250 100644 --- a/packages/shorebird_cli/lib/src/commands/login_command.dart +++ b/packages/shorebird_cli/lib/src/commands/login_command.dart @@ -18,32 +18,37 @@ class LoginCommand extends ShorebirdCommand { @override Future run() async { - final session = auth.currentSession; - if (session != null) { + final credentials = auth.credentials; + if (credentials != null) { logger ..info('You are already logged in.') ..info("Run 'shorebird logout' to log out and try again."); return ExitCode.success.code; } - final apiKey = logger.prompt( - '${lightGreen.wrap('?')} Please enter your API Key:', - ); - final loginProgress = logger.progress('Logging into shorebird.dev'); try { - auth.login(apiKey: apiKey); - loginProgress.complete(); + await auth.login(prompt); logger.info(''' 🎉 ${lightGreen.wrap('Welcome to Shorebird! You are now logged in.')} -🔑 Credentials are stored in ${lightCyan.wrap(auth.sessionFilePath)}. +🔑 Credentials are stored in ${lightCyan.wrap(auth.credentialsFilePath)}. 🚪 To logout use: "${lightCyan.wrap('shorebird logout')}".'''); return ExitCode.success.code; } catch (error) { - loginProgress.fail(); logger.err(error.toString()); return ExitCode.software.code; } } + + void prompt(String url) { + logger.info(''' +The Shorebird CLI needs your authorization to manage apps, releases, and patches on your behalf. + +In a browser, visit this URL to log in: + +${styleBold.wrap(styleUnderlined.wrap(lightCyan.wrap(url)))} + +Waiting for your authorization...'''); + } } diff --git a/packages/shorebird_cli/lib/src/commands/logout_command.dart b/packages/shorebird_cli/lib/src/commands/logout_command.dart index 3c2fc81f8..bbe7703d8 100644 --- a/packages/shorebird_cli/lib/src/commands/logout_command.dart +++ b/packages/shorebird_cli/lib/src/commands/logout_command.dart @@ -18,8 +18,7 @@ class LogoutCommand extends ShorebirdCommand { @override Future run() async { - final session = auth.currentSession; - if (session == null) { + if (auth.credentials == null) { logger.info('You are already logged out.'); return ExitCode.success.code; } diff --git a/packages/shorebird_cli/lib/src/commands/patch_command.dart b/packages/shorebird_cli/lib/src/commands/patch_command.dart index f46ec1baf..a183723d2 100644 --- a/packages/shorebird_cli/lib/src/commands/patch_command.dart +++ b/packages/shorebird_cli/lib/src/commands/patch_command.dart @@ -93,8 +93,7 @@ class PatchCommand extends ShorebirdCommand return ExitCode.config.code; } - final session = auth.currentSession; - if (session == null) { + if (auth.credentials == null) { logger.err('You must be logged in to publish.'); return ExitCode.noUser.code; } @@ -147,7 +146,7 @@ class PatchCommand extends ShorebirdCommand final pubspecYaml = getPubspecYaml()!; final shorebirdYaml = getShorebirdYaml()!; final codePushClient = buildCodePushClient( - apiKey: session.apiKey, + httpClient: auth.client, hostedUri: hostedUri, ); final version = pubspecYaml.version!; diff --git a/packages/shorebird_cli/lib/src/commands/release_command.dart b/packages/shorebird_cli/lib/src/commands/release_command.dart index b9605d29a..0cec23e1a 100644 --- a/packages/shorebird_cli/lib/src/commands/release_command.dart +++ b/packages/shorebird_cli/lib/src/commands/release_command.dart @@ -71,8 +71,7 @@ make smaller updates to your app. return ExitCode.config.code; } - final session = auth.currentSession; - if (session == null) { + if (auth.credentials == null) { logger.err('You must be logged in to release.'); return ExitCode.noUser.code; } @@ -117,7 +116,7 @@ make smaller updates to your app. final pubspecYaml = getPubspecYaml()!; final shorebirdYaml = getShorebirdYaml()!; final codePushClient = buildCodePushClient( - apiKey: session.apiKey, + httpClient: auth.client, hostedUri: hostedUri, ); final version = pubspecYaml.version!; diff --git a/packages/shorebird_cli/lib/src/commands/run_command.dart b/packages/shorebird_cli/lib/src/commands/run_command.dart index a30f397f3..44ca68be8 100644 --- a/packages/shorebird_cli/lib/src/commands/run_command.dart +++ b/packages/shorebird_cli/lib/src/commands/run_command.dart @@ -34,7 +34,7 @@ class RunCommand extends ShorebirdCommand @override Future run() async { - if (auth.currentSession == null) { + if (auth.credentials == null) { logger ..err('You must be logged in to run.') ..err("Run 'shorebird login' to log in and try again."); diff --git a/packages/shorebird_cli/lib/src/shorebird_create_app_mixin.dart b/packages/shorebird_cli/lib/src/shorebird_create_app_mixin.dart index 720642617..bf6f06775 100644 --- a/packages/shorebird_cli/lib/src/shorebird_create_app_mixin.dart +++ b/packages/shorebird_cli/lib/src/shorebird_create_app_mixin.dart @@ -20,7 +20,7 @@ mixin ShorebirdCreateAppMixin on ShorebirdConfigMixin { } final client = buildCodePushClient( - apiKey: auth.currentSession!.apiKey, + httpClient: auth.client, hostedUri: hostedUri, ); diff --git a/packages/shorebird_cli/lib/src/shorebird_engine_mixin.dart b/packages/shorebird_cli/lib/src/shorebird_engine_mixin.dart index 380452599..d31174673 100644 --- a/packages/shorebird_cli/lib/src/shorebird_engine_mixin.dart +++ b/packages/shorebird_cli/lib/src/shorebird_engine_mixin.dart @@ -32,7 +32,7 @@ mixin ShorebirdEngineMixin on ShorebirdConfigMixin { final engineArchivePath = p.join(tempDir.path, 'engine.zip'); try { final codePushClient = buildCodePushClient( - apiKey: auth.currentSession!.apiKey, + httpClient: auth.client, hostedUri: hostedUri, ); await _downloadShorebirdEngine(codePushClient, engineArchivePath); diff --git a/packages/shorebird_cli/pubspec.yaml b/packages/shorebird_cli/pubspec.yaml index 6b3108ba3..6b1b189ed 100644 --- a/packages/shorebird_cli/pubspec.yaml +++ b/packages/shorebird_cli/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: cli_util: ^0.4.0 collection: ^1.17.1 crypto: ^3.0.2 + googleapis_auth: ^1.4.0 http: ^0.13.5 json_annotation: ^4.8.0 mason_logger: ^0.2.4 diff --git a/packages/shorebird_cli/test/src/auth/auth_test.dart b/packages/shorebird_cli/test/src/auth/auth_test.dart index b6b1c2f43..e850abd18 100644 --- a/packages/shorebird_cli/test/src/auth/auth_test.dart +++ b/packages/shorebird_cli/test/src/auth/auth_test.dart @@ -1,42 +1,95 @@ +import 'package:googleapis_auth/googleapis_auth.dart'; +import 'package:http/http.dart' as http; +import 'package:mocktail/mocktail.dart'; import 'package:shorebird_cli/src/auth/auth.dart'; -import 'package:shorebird_cli/src/auth/session.dart'; import 'package:test/test.dart'; +class _MockHttpClient extends Mock implements http.Client {} + void main() { group('Auth', () { - const apiKey = 'test-api-key'; + final credentials = AccessCredentials( + AccessToken('Bearer', 'token', DateTime.now().toUtc()), + 'refreshToken', + [], + ); + late http.Client httpClient; late Auth auth; setUp(() { - auth = Auth()..logout(); + httpClient = _MockHttpClient(); + auth = Auth( + httpClient: httpClient, + obtainAccessCredentials: (clientId, scopes, client, userPrompt) async { + return credentials; + }, + )..logout(); + }); + + group('client', () { + test( + 'returns an auto-refreshing client ' + 'when credentials are present.', () async { + await auth.login((_) {}); + final client = auth.client; + expect(client, isA()); + expect(client, isA()); + }); + + test( + 'returns a plain http client ' + 'when credentials are not present.', () async { + final client = auth.client; + expect(client, isA()); + expect(client, isNot(isA())); + }); }); group('login', () { - test('should set the current session', () { - auth.login(apiKey: apiKey); + test('should set the credentials', () async { + await auth.login((_) {}); expect( - auth.currentSession, - isA().having((s) => s.apiKey, 'apiKey', apiKey), + auth.credentials, + isA().having( + (c) => c.accessToken.data, + 'accessToken', + credentials.accessToken.data, + ), ); expect( - Auth().currentSession, - isA().having((s) => s.apiKey, 'apiKey', apiKey), + Auth().credentials, + isA().having( + (c) => c.accessToken.data, + 'accessToken', + credentials.accessToken.data, + ), ); }); }); group('logout', () { - test('clears session and wipes state', () { - auth.login(apiKey: apiKey); + test('clears session and wipes state', () async { + await auth.login((_) {}); expect( - auth.currentSession, - isA().having((s) => s.apiKey, 'apiKey', apiKey), + auth.credentials, + isA().having( + (c) => c.accessToken.data, + 'accessToken', + credentials.accessToken.data, + ), ); auth.logout(); - expect(auth.currentSession, isNull); - expect(Auth().currentSession, isNull); + expect(auth.credentials, isNull); + expect(Auth().credentials, isNull); + }); + }); + + group('close', () { + test('closes the underlying httpClient', () { + auth.close(); + verify(() => httpClient.close()).called(1); }); }); }); diff --git a/packages/shorebird_cli/test/src/commands/apps/create_apps_command_test.dart b/packages/shorebird_cli/test/src/commands/apps/create_apps_command_test.dart index 49ac2d428..c5a4b92cf 100644 --- a/packages/shorebird_cli/test/src/commands/apps/create_apps_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/apps/create_apps_command_test.dart @@ -1,28 +1,32 @@ 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/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 _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 _MockAccessCredentials extends Mock implements AccessCredentials {} + void main() { group('create', () { - const apiKey = 'test-api-key'; const appId = 'app-id'; const displayName = 'Example App'; - const session = Session(apiKey: apiKey); + final credentials = _MockAccessCredentials(); late ArgResults argResults; + late http.Client httpClient; late Auth auth; late Logger logger; late CodePushClient codePushClient; @@ -30,18 +34,23 @@ void main() { setUp(() { argResults = _MockArgResults(); + httpClient = _MockHttpClient(); auth = _MockAuth(); logger = _MockLogger(); codePushClient = _MockCodePushClient(); command = CreateAppCommand( auth: auth, - buildCodePushClient: ({required String apiKey, Uri? hostedUri}) { + buildCodePushClient: ({ + required http.Client httpClient, + Uri? hostedUri, + }) { return codePushClient; }, logger: logger, )..testArgResults = argResults; - when(() => auth.currentSession).thenReturn(session); + when(() => auth.credentials).thenReturn(credentials); + when(() => auth.client).thenReturn(httpClient); }); test('returns correct description', () { @@ -49,7 +58,7 @@ void main() { }); test('returns no user error when not logged in', () async { - when(() => auth.currentSession).thenReturn(null); + when(() => auth.credentials).thenReturn(null); final result = await command.run(); expect(result, ExitCode.noUser.code); }); diff --git a/packages/shorebird_cli/test/src/commands/apps/delete_apps_command_test.dart b/packages/shorebird_cli/test/src/commands/apps/delete_apps_command_test.dart index 88eb9249b..daf56fa18 100644 --- a/packages/shorebird_cli/test/src/commands/apps/delete_apps_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/apps/delete_apps_command_test.dart @@ -1,14 +1,18 @@ 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/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 _MockAccessCredentials extends Mock implements AccessCredentials {} + 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 {} @@ -17,11 +21,11 @@ class _MockLogger extends Mock implements Logger {} void main() { group('delete', () { - const apiKey = 'test-api-key'; const appId = 'example'; - const session = Session(apiKey: apiKey); + final credentials = _MockAccessCredentials(); late ArgResults argResults; + late http.Client httpClient; late Auth auth; late Logger logger; late CodePushClient codePushClient; @@ -29,18 +33,23 @@ void main() { setUp(() { argResults = _MockArgResults(); + httpClient = _MockHttpClient(); auth = _MockAuth(); logger = _MockLogger(); codePushClient = _MockCodePushClient(); command = DeleteAppCommand( auth: auth, - buildCodePushClient: ({required String apiKey, Uri? hostedUri}) { + buildCodePushClient: ({ + required http.Client httpClient, + Uri? hostedUri, + }) { return codePushClient; }, logger: logger, )..testArgResults = argResults; - when(() => auth.currentSession).thenReturn(session); + when(() => auth.credentials).thenReturn(credentials); + when(() => auth.client).thenReturn(httpClient); }); test('returns correct description', () { @@ -51,7 +60,7 @@ void main() { }); test('returns no user error when not logged in', () async { - when(() => auth.currentSession).thenReturn(null); + when(() => auth.credentials).thenReturn(null); final result = await command.run(); expect(result, ExitCode.noUser.code); }); diff --git a/packages/shorebird_cli/test/src/commands/apps/list_apps_command_test.dart b/packages/shorebird_cli/test/src/commands/apps/list_apps_command_test.dart index 977dd3711..7bb524463 100644 --- a/packages/shorebird_cli/test/src/commands/apps/list_apps_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/apps/list_apps_command_test.dart @@ -1,11 +1,15 @@ +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/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 _MockAccessCredentials extends Mock implements AccessCredentials {} + +class _MockHttpClient extends Mock implements http.Client {} + class _MockAuth extends Mock implements Auth {} class _MockCodePushClient extends Mock implements CodePushClient {} @@ -14,8 +18,9 @@ class _MockLogger extends Mock implements Logger {} void main() { group('list', () { - const session = Session(apiKey: 'test-api-key'); + final credentials = _MockAccessCredentials(); + late http.Client httpClient; late Auth auth; late CodePushClient codePushClient; late Logger logger; @@ -23,18 +28,23 @@ void main() { late ListAppsCommand command; setUp(() { + httpClient = _MockHttpClient(); auth = _MockAuth(); codePushClient = _MockCodePushClient(); logger = _MockLogger(); command = ListAppsCommand( auth: auth, - buildCodePushClient: ({required String apiKey, Uri? hostedUri}) { + buildCodePushClient: ({ + required http.Client httpClient, + Uri? hostedUri, + }) { return codePushClient; }, logger: logger, ); - when(() => auth.currentSession).thenReturn(session); + when(() => auth.credentials).thenReturn(credentials); + when(() => auth.client).thenReturn(httpClient); }); test('description is correct', () { @@ -42,7 +52,7 @@ void main() { }); test('returns ExitCode.noUser when not logged in', () async { - when(() => auth.currentSession).thenReturn(null); + when(() => auth.credentials).thenReturn(null); expect(await command.run(), ExitCode.noUser.code); }); diff --git a/packages/shorebird_cli/test/src/commands/build_command_test.dart b/packages/shorebird_cli/test/src/commands/build_command_test.dart index 170877a1e..8e3af8df2 100644 --- a/packages/shorebird_cli/test/src/commands/build_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/build_command_test.dart @@ -3,10 +3,10 @@ import 'dart:typed_data'; import 'package:archive/archive_io.dart'; 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/auth/session.dart'; import 'package:shorebird_cli/src/commands/build_command.dart'; import 'package:shorebird_cli/src/config/config.dart'; import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; @@ -14,6 +14,10 @@ import 'package:test/test.dart'; class _MockArgResults extends Mock implements ArgResults {} +class _MockAccessCredentials extends Mock implements AccessCredentials {} + +class _MockHttpClient extends Mock implements http.Client {} + class _MockAuth extends Mock implements Auth {} class _MockLogger extends Mock implements Logger {} @@ -26,10 +30,11 @@ class _MockCodePushClient extends Mock implements CodePushClient {} void main() { group('build', () { - const session = Session(apiKey: 'test-api-key'); + final credentials = _MockAccessCredentials(); late ArgResults argResults; late Directory applicationConfigHome; + late http.Client httpClient; late Auth auth; late CodePushClient codePushClient; late Logger logger; @@ -39,13 +44,17 @@ void main() { setUp(() { applicationConfigHome = Directory.systemTemp.createTempSync(); argResults = _MockArgResults(); + httpClient = _MockHttpClient(); auth = _MockAuth(); codePushClient = _MockCodePushClient(); logger = _MockLogger(); processResult = _MockProcessResult(); buildCommand = BuildCommand( auth: auth, - buildCodePushClient: ({required String apiKey, Uri? hostedUri}) { + buildCodePushClient: ({ + required http.Client httpClient, + Uri? hostedUri, + }) { return codePushClient; }, logger: logger, @@ -61,6 +70,8 @@ void main() { testApplicationConfigHome = (_) => applicationConfigHome.path; when(() => argResults.rest).thenReturn([]); + when(() => auth.credentials).thenReturn(credentials); + when(() => auth.client).thenReturn(httpClient); when( () => codePushClient.downloadEngine(revision: any(named: 'revision')), ).thenAnswer((_) async => Uint8List.fromList([])); @@ -68,7 +79,7 @@ void main() { }); test('exits with no user when not logged in', () async { - when(() => auth.currentSession).thenReturn(null); + when(() => auth.credentials).thenReturn(null); final result = await buildCommand.run(); expect(result, equals(ExitCode.noUser.code)); @@ -83,7 +94,6 @@ void main() { when( () => codePushClient.downloadEngine(revision: any(named: 'revision')), ).thenThrow(Exception('oops')); - when(() => auth.currentSession).thenReturn(session); final result = await buildCommand.run(); @@ -94,7 +104,6 @@ void main() { when(() => processResult.exitCode).thenReturn(1); when(() => processResult.stderr).thenReturn('oops'); final tempDir = Directory.systemTemp.createTempSync(); - when(() => auth.currentSession).thenReturn(session); when( () => codePushClient.downloadEngine(revision: any(named: 'revision')), ).thenAnswer( @@ -117,7 +126,6 @@ void main() { ).thenAnswer( (_) async => Uint8List.fromList(ZipEncoder().encode(Archive())!), ); - when(() => auth.currentSession).thenReturn(session); final result = await IOOverrides.runZoned( () async => buildCommand.run(), diff --git a/packages/shorebird_cli/test/src/commands/channels/create_channels_command_test.dart b/packages/shorebird_cli/test/src/commands/channels/create_channels_command_test.dart index 1cebe5c6b..09006ecf0 100644 --- a/packages/shorebird_cli/test/src/commands/channels/create_channels_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/channels/create_channels_command_test.dart @@ -1,14 +1,18 @@ 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/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 _MockAccessCredentials extends Mock implements AccessCredentials {} + 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 {} @@ -19,12 +23,14 @@ class _MockProgress extends Mock implements Progress {} void main() { group('create', () { - const session = Session(apiKey: 'test-api-key'); const appId = 'test-app-id'; const channelName = 'my-channel'; const channel = Channel(id: 0, appId: appId, name: channelName); + final credentials = _MockAccessCredentials(); + late ArgResults argResults; + late http.Client httpClient; late Auth auth; late CodePushClient codePushClient; late Logger logger; @@ -33,13 +39,17 @@ void main() { setUp(() { argResults = _MockArgResults(); + httpClient = _MockHttpClient(); auth = _MockAuth(); codePushClient = _MockCodePushClient(); logger = _MockLogger(); progress = _MockProgress(); command = CreateChannelsCommand( auth: auth, - buildCodePushClient: ({required String apiKey, Uri? hostedUri}) { + buildCodePushClient: ({ + required http.Client httpClient, + Uri? hostedUri, + }) { return codePushClient; }, logger: logger, @@ -47,7 +57,8 @@ void main() { when(() => argResults['app-id']).thenReturn(appId); when(() => argResults['name']).thenReturn(channelName); - when(() => auth.currentSession).thenReturn(session); + when(() => auth.credentials).thenReturn(credentials); + when(() => auth.client).thenReturn(httpClient); when(() => logger.confirm(any())).thenReturn(true); when(() => logger.progress(any())).thenReturn(progress); }); @@ -60,7 +71,7 @@ void main() { }); test('returns ExitCode.noUser when not logged in', () async { - when(() => auth.currentSession).thenReturn(null); + when(() => auth.credentials).thenReturn(null); expect(await command.run(), ExitCode.noUser.code); }); diff --git a/packages/shorebird_cli/test/src/commands/channels/list_channels_command_test.dart b/packages/shorebird_cli/test/src/commands/channels/list_channels_command_test.dart index 6b7f1a3e6..b4f10683b 100644 --- a/packages/shorebird_cli/test/src/commands/channels/list_channels_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/channels/list_channels_command_test.dart @@ -1,14 +1,18 @@ 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/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 _MockAccessCredentials extends Mock implements AccessCredentials {} + 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 {} @@ -17,10 +21,11 @@ class _MockLogger extends Mock implements Logger {} void main() { group('list', () { - const session = Session(apiKey: 'test-api-key'); const appId = 'test-app-id'; + final credentials = _MockAccessCredentials(); late ArgResults argResults; + late http.Client httpClient; late Auth auth; late CodePushClient codePushClient; late Logger logger; @@ -28,19 +33,24 @@ void main() { setUp(() { argResults = _MockArgResults(); + httpClient = _MockHttpClient(); auth = _MockAuth(); codePushClient = _MockCodePushClient(); logger = _MockLogger(); command = ListChannelsCommand( auth: auth, - buildCodePushClient: ({required String apiKey, Uri? hostedUri}) { + buildCodePushClient: ({ + required http.Client httpClient, + Uri? hostedUri, + }) { return codePushClient; }, logger: logger, )..testArgResults = argResults; when(() => argResults['app-id']).thenReturn(appId); - when(() => auth.currentSession).thenReturn(session); + when(() => auth.credentials).thenReturn(credentials); + when(() => auth.client).thenReturn(httpClient); }); test('description is correct', () { @@ -51,7 +61,7 @@ void main() { }); test('returns ExitCode.noUser when not logged in', () async { - when(() => auth.currentSession).thenReturn(null); + when(() => auth.credentials).thenReturn(null); expect(await command.run(), ExitCode.noUser.code); }); diff --git a/packages/shorebird_cli/test/src/commands/doctor_command_test.dart b/packages/shorebird_cli/test/src/commands/doctor_command_test.dart index fe8c3d807..2d926d62d 100644 --- a/packages/shorebird_cli/test/src/commands/doctor_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/doctor_command_test.dart @@ -60,7 +60,7 @@ void main() { verify(validator.validate).called(1); } verify( - () => logger.info(captureAny(that: contains('No issues detected'))), + () => logger.info(any(that: contains('No issues detected'))), ).called(1); }); @@ -87,27 +87,15 @@ void main() { } verify( - () => logger.info( - captureAny( - that: contains('[!] oh no!'), - ), - ), + () => logger.info(any(that: contains('${yellow.wrap('[!]')} oh no!'))), ).called(1); verify( - () => logger.info( - captureAny( - that: contains('[✗] OH NO!'), - ), - ), + () => logger.info(any(that: contains('${red.wrap('[✗]')} OH NO!'))), ).called(1); verify( - () => logger.info( - captureAny( - that: contains('2 issues detected.'), - ), - ), + () => logger.info(any(that: contains('2 issues detected.'))), ).called(1); }); }); diff --git a/packages/shorebird_cli/test/src/commands/init_command_test.dart b/packages/shorebird_cli/test/src/commands/init_command_test.dart index 8a237ea45..388db386d 100644 --- a/packages/shorebird_cli/test/src/commands/init_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/init_command_test.dart @@ -1,14 +1,18 @@ import 'dart:io'; +import 'package:http/http.dart' as http; import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; import 'package:path/path.dart' as p; import 'package:shorebird_cli/src/auth/auth.dart'; -import 'package:shorebird_cli/src/auth/session.dart'; import 'package:shorebird_cli/src/commands/init_command.dart'; import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; import 'package:test/test.dart'; +class _MockAccessCredentials extends Mock implements AccessCredentials {} + +class _MockHttpClient extends Mock implements http.Client {} + class _MockAuth extends Mock implements Auth {} class _MockCodePushClient extends Mock implements CodePushClient {} @@ -19,7 +23,6 @@ class _MockProgress extends Mock implements Progress {} void main() { group('init', () { - const apiKey = 'test-api-key'; const version = '1.2.3'; const appId = 'test_app_id'; const appName = 'test_app_name'; @@ -30,8 +33,10 @@ name: $appName version: $version environment: sdk: ">=2.19.0 <3.0.0"'''; - const session = Session(apiKey: apiKey); + final credentials = _MockAccessCredentials(); + + late http.Client httpClient; late Auth auth; late CodePushClient codePushClient; late Logger logger; @@ -39,19 +44,24 @@ environment: late InitCommand command; setUp(() { + httpClient = _MockHttpClient(); auth = _MockAuth(); codePushClient = _MockCodePushClient(); logger = _MockLogger(); progress = _MockProgress(); command = InitCommand( auth: auth, - buildCodePushClient: ({required String apiKey, Uri? hostedUri}) { + buildCodePushClient: ({ + required http.Client httpClient, + Uri? hostedUri, + }) { return codePushClient; }, logger: logger, ); - when(() => auth.currentSession).thenReturn(session); + when(() => auth.credentials).thenReturn(credentials); + when(() => auth.client).thenReturn(httpClient); when( () => codePushClient.createApp(displayName: any(named: 'displayName')), ).thenAnswer((_) async => app); @@ -65,7 +75,7 @@ environment: }); test('returns no user error when not logged in', () async { - when(() => auth.currentSession).thenReturn(null); + when(() => auth.credentials).thenReturn(null); final result = await command.run(); expect(result, ExitCode.noUser.code); }); diff --git a/packages/shorebird_cli/test/src/commands/login_command_test.dart b/packages/shorebird_cli/test/src/commands/login_command_test.dart index c96cc8786..b2f31967b 100644 --- a/packages/shorebird_cli/test/src/commands/login_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/login_command_test.dart @@ -4,21 +4,19 @@ import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; import 'package:path/path.dart' as p; import 'package:shorebird_cli/src/auth/auth.dart'; -import 'package:shorebird_cli/src/auth/session.dart'; import 'package:shorebird_cli/src/commands/login_command.dart'; import 'package:shorebird_cli/src/config/config.dart'; import 'package:test/test.dart'; +class _MockAccessCredentials extends Mock implements AccessCredentials {} + class _MockAuth extends Mock implements Auth {} class _MockLogger extends Mock implements Logger {} -class _MockProgress extends Mock implements Progress {} - void main() { group('login', () { - const apiKey = 'test-api-key'; - const session = Session(apiKey: apiKey); + final credentials = _MockAccessCredentials(); late Directory applicationConfigHome; late Logger logger; @@ -33,14 +31,13 @@ void main() { testApplicationConfigHome = (_) => applicationConfigHome.path; - when(() => logger.progress(any())).thenReturn(_MockProgress()); - when(() => auth.sessionFilePath).thenReturn( - p.join(applicationConfigHome.path, 'shorebird-session.json'), + when(() => auth.credentialsFilePath).thenReturn( + p.join(applicationConfigHome.path, 'credentials.json'), ); }); test('exits with code 0 when already logged in', () async { - when(() => auth.currentSession).thenReturn(session); + when(() => auth.credentials).thenReturn(credentials); final result = await loginCommand.run(); expect(result, equals(ExitCode.success.code)); @@ -53,35 +50,41 @@ void main() { test('exits with code 70 when error occurs', () async { final error = Exception('oops something went wrong!'); - when(() => logger.prompt(any())).thenReturn(apiKey); - when(() => auth.currentSession).thenReturn(null); - when( - () => auth.login(apiKey: any(named: 'apiKey')), - ).thenThrow(error); + when(() => auth.login(any())).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)).called(1); + verify(() => auth.login(any())).called(1); verify(() => logger.err(error.toString())).called(1); }); test('exits with code 0 when logged in successfully', () async { - when(() => logger.prompt(any())).thenReturn(apiKey); - when(() => auth.currentSession).thenReturn(null); - when( - () => auth.login(apiKey: any(named: 'apiKey')), - ).thenAnswer((_) async {}); + when(() => auth.login(any())).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)).called(1); + verify(() => auth.login(any())).called(1); verify( () => logger.info(any(that: contains('You are now logged in.'))), ).called(1); }); + + test('prompt is correct', () { + const url = 'http://example.com'; + loginCommand.prompt(url); + + verify( + () => logger.info(''' +The Shorebird CLI needs your authorization to manage apps, releases, and patches on your behalf. + +In a browser, visit this URL to log in: + +${styleBold.wrap(styleUnderlined.wrap(lightCyan.wrap(url)))} + +Waiting for your authorization...'''), + ).called(1); + }); }); } diff --git a/packages/shorebird_cli/test/src/commands/logout_command_test.dart b/packages/shorebird_cli/test/src/commands/logout_command_test.dart index dc92cfe57..4184797d6 100644 --- a/packages/shorebird_cli/test/src/commands/logout_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/logout_command_test.dart @@ -1,10 +1,11 @@ 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/logout_command.dart'; import 'package:test/test.dart'; +class _MockAccessCredentials extends Mock implements AccessCredentials {} + class _MockLogger extends Mock implements Logger {} class _MockAuth extends Mock implements Auth {} @@ -35,8 +36,8 @@ void main() { }); test('exits with code 0 when logged out successfully', () async { - const session = Session(apiKey: 'test-api-key'); - when(() => auth.currentSession).thenReturn(session); + final credentials = _MockAccessCredentials(); + when(() => auth.credentials).thenReturn(credentials); final progress = _MockProgress(); when(() => progress.complete(any())).thenAnswer((invocation) {}); diff --git a/packages/shorebird_cli/test/src/commands/patch_command_test.dart b/packages/shorebird_cli/test/src/commands/patch_command_test.dart index 6e4c672eb..d085f0c3b 100644 --- a/packages/shorebird_cli/test/src/commands/patch_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/patch_command_test.dart @@ -8,12 +8,13 @@ import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; import 'package:path/path.dart' as p; import 'package:shorebird_cli/src/auth/auth.dart'; -import 'package:shorebird_cli/src/auth/session.dart'; import 'package:shorebird_cli/src/commands/patch_command.dart'; import 'package:shorebird_cli/src/config/config.dart'; import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; import 'package:test/test.dart'; +class _MockAccessCredentials extends Mock implements AccessCredentials {} + class _FakeBaseRequest extends Fake implements http.BaseRequest {} class _MockArgResults extends Mock implements ArgResults {} @@ -32,7 +33,6 @@ class _MockCodePushClient extends Mock implements CodePushClient {} void main() { group('patch', () { - const session = Session(apiKey: 'test-api-key'); const appId = 'test-app-id'; const version = '1.2.3'; const arch = 'aarch64'; @@ -76,6 +76,8 @@ flutter: assets: - shorebird.yaml'''; + final credentials = _MockAccessCredentials(); + late ArgResults argResults; late Directory applicationConfigHome; late Auth auth; @@ -115,7 +117,10 @@ flutter: codePushClient = _MockCodePushClient(); command = PatchCommand( auth: auth, - buildCodePushClient: ({required String apiKey, Uri? hostedUri}) { + buildCodePushClient: ({ + required http.Client httpClient, + Uri? hostedUri, + }) { capturedHostedUri = hostedUri; return codePushClient; }, @@ -140,7 +145,8 @@ flutter: when(() => argResults['channel']).thenReturn(channelName); when(() => argResults['dry-run']).thenReturn(false); when(() => argResults['force']).thenReturn(false); - when(() => auth.currentSession).thenReturn(session); + when(() => auth.credentials).thenReturn(credentials); + when(() => auth.client).thenReturn(httpClient); when(() => logger.progress(any())).thenReturn(progress); when( () => logger.prompt(any(), defaultValue: any(named: 'defaultValue')), @@ -213,7 +219,7 @@ flutter: }); test('throws no user error when session does not exist', () async { - when(() => auth.currentSession).thenReturn(null); + when(() => auth.credentials).thenReturn(null); final tempDir = setUpTempDir(); final exitCode = await IOOverrides.runZoned( () => command.run(), @@ -226,7 +232,6 @@ flutter: when( () => codePushClient.downloadEngine(revision: any(named: 'revision')), ).thenThrow(Exception('oops')); - when(() => auth.currentSession).thenReturn(session); final tempDir = setUpTempDir(); final exitCode = await IOOverrides.runZoned( command.run, @@ -238,7 +243,6 @@ flutter: test('exits with code 70 when building fails', () async { when(() => flutterBuildProcessResult.exitCode).thenReturn(1); when(() => flutterBuildProcessResult.stderr).thenReturn('oops'); - when(() => auth.currentSession).thenReturn(session); when( () => codePushClient.downloadEngine(revision: any(named: 'revision')), @@ -260,7 +264,6 @@ flutter: 'both --dry-run and --force are specified', () async { when(() => argResults['dry-run']).thenReturn(true); when(() => argResults['force']).thenReturn(true); - when(() => auth.currentSession).thenReturn(session); final tempDir = setUpTempDir(); Directory( p.join(command.shorebirdEnginePath, 'engine'), diff --git a/packages/shorebird_cli/test/src/commands/release_command_test.dart b/packages/shorebird_cli/test/src/commands/release_command_test.dart index 51b3b5ca2..d0e9c85dd 100644 --- a/packages/shorebird_cli/test/src/commands/release_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/release_command_test.dart @@ -3,18 +3,22 @@ import 'dart:typed_data'; import 'package:archive/archive.dart'; 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:path/path.dart' as p; 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_cli/src/config/config.dart'; import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; import 'package:test/test.dart'; +class _MockAccessCredentials extends Mock implements AccessCredentials {} + class _MockArgResults extends Mock implements ArgResults {} +class _MockHttpClient extends Mock implements http.Client {} + class _MockAuth extends Mock implements Auth {} class _MockLogger extends Mock implements Logger {} @@ -27,7 +31,6 @@ class _MockCodePushClient extends Mock implements CodePushClient {} void main() { group('release', () { - const session = Session(apiKey: 'test-api-key'); const appId = 'test-app-id'; const version = '1.2.3'; const appDisplayName = 'Test App'; @@ -60,8 +63,11 @@ flutter: assets: - shorebird.yaml'''; + final credentials = _MockAccessCredentials(); + late ArgResults argResults; late Directory applicationConfigHome; + late http.Client httpClient; late Auth auth; late Progress progress; late Logger logger; @@ -84,6 +90,7 @@ flutter: setUp(() { argResults = _MockArgResults(); applicationConfigHome = Directory.systemTemp.createTempSync(); + httpClient = _MockHttpClient(); auth = _MockAuth(); progress = _MockProgress(); logger = _MockLogger(); @@ -91,7 +98,10 @@ flutter: codePushClient = _MockCodePushClient(); command = ReleaseCommand( auth: auth, - buildCodePushClient: ({required String apiKey, Uri? hostedUri}) { + buildCodePushClient: ({ + required http.Client httpClient, + Uri? hostedUri, + }) { capturedHostedUri = hostedUri; return codePushClient; }, @@ -110,7 +120,8 @@ flutter: when(() => argResults.rest).thenReturn([]); when(() => argResults['arch']).thenReturn(arch); when(() => argResults['platform']).thenReturn(platform); - when(() => auth.currentSession).thenReturn(session); + when(() => auth.credentials).thenReturn(credentials); + when(() => auth.client).thenReturn(httpClient); when(() => logger.progress(any())).thenReturn(progress); when(() => logger.confirm(any())).thenReturn(true); when( @@ -158,7 +169,7 @@ flutter: }); test('throws no user error when session does not exist', () async { - when(() => auth.currentSession).thenReturn(null); + when(() => auth.credentials).thenReturn(null); final tempDir = setUpTempDir(); final exitCode = await IOOverrides.runZoned( () => command.run(), @@ -171,7 +182,6 @@ flutter: when( () => codePushClient.downloadEngine(revision: any(named: 'revision')), ).thenThrow(Exception('oops')); - when(() => auth.currentSession).thenReturn(session); final tempDir = setUpTempDir(); final exitCode = await IOOverrides.runZoned( command.run, @@ -183,8 +193,6 @@ flutter: test('exits with code 70 when building fails', () async { when(() => processResult.exitCode).thenReturn(1); when(() => processResult.stderr).thenReturn('oops'); - when(() => auth.currentSession).thenReturn(session); - when( () => codePushClient.downloadEngine(revision: any(named: 'revision')), ).thenAnswer( diff --git a/packages/shorebird_cli/test/src/commands/run_command_test.dart b/packages/shorebird_cli/test/src/commands/run_command_test.dart index a83205fcc..a8494c444 100644 --- a/packages/shorebird_cli/test/src/commands/run_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/run_command_test.dart @@ -5,18 +5,22 @@ import 'dart:typed_data'; import 'package:archive/archive_io.dart'; 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:path/path.dart' as p; import 'package:shorebird_cli/src/auth/auth.dart'; -import 'package:shorebird_cli/src/auth/session.dart'; import 'package:shorebird_cli/src/commands/run_command.dart'; import 'package:shorebird_cli/src/config/config.dart'; import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; import 'package:test/test.dart'; +class _MockAccessCredentials extends Mock implements AccessCredentials {} + class _MockArgResults extends Mock implements ArgResults {} +class _MockHttpClient extends Mock implements http.Client {} + class _MockAuth extends Mock implements Auth {} class _MockLogger extends Mock implements Logger {} @@ -29,10 +33,11 @@ class _MockCodePushClient extends Mock implements CodePushClient {} void main() { group('run', () { - const session = Session(apiKey: 'test-api-key'); + final credentials = _MockAccessCredentials(); late ArgResults argResults; late Directory applicationConfigHome; + late http.Client httpClient; late Auth auth; late Logger logger; late Process process; @@ -42,6 +47,7 @@ void main() { setUp(() { argResults = _MockArgResults(); applicationConfigHome = Directory.systemTemp.createTempSync(); + httpClient = _MockHttpClient(); auth = _MockAuth(); logger = _MockLogger(); process = _MockProcess(); @@ -49,7 +55,10 @@ void main() { runCommand = RunCommand( auth: auth, logger: logger, - buildCodePushClient: ({required String apiKey, Uri? hostedUri}) { + buildCodePushClient: ({ + required http.Client httpClient, + Uri? hostedUri, + }) { return codePushClient; }, startProcess: (executable, arguments, {bool runInShell = false}) async { @@ -60,11 +69,13 @@ void main() { testApplicationConfigHome = (_) => applicationConfigHome.path; when(() => argResults.rest).thenReturn([]); + when(() => auth.credentials).thenReturn(credentials); + when(() => auth.client).thenReturn(httpClient); when(() => logger.progress(any())).thenReturn(_MockProgress()); }); test('exits with no user when not logged in', () async { - when(() => auth.currentSession).thenReturn(null); + when(() => auth.credentials).thenReturn(null); final result = await runCommand.run(); expect(result, equals(ExitCode.noUser.code)); @@ -77,7 +88,6 @@ void main() { test('exits with code 70 when downloading engine fails', () async { final error = Exception('oops'); - when(() => auth.currentSession).thenReturn(session); when( () => codePushClient.downloadEngine(revision: any(named: 'revision')), ).thenThrow(error); @@ -98,7 +108,6 @@ void main() { test('exits with code 70 when building the engine fails', () async { final tempDir = Directory.systemTemp.createTempSync(); - when(() => auth.currentSession).thenReturn(session); when( () => codePushClient.downloadEngine(revision: any(named: 'revision')), ).thenAnswer((_) async => Uint8List(0)); @@ -126,7 +135,6 @@ void main() { ..create(p.join(runCommand.shorebirdEnginePath, 'engine.zip')) ..close(); - when(() => auth.currentSession).thenReturn(session); when( () => codePushClient.downloadEngine(revision: any(named: 'revision')), ).thenAnswer((_) async => Uint8List(0)); @@ -159,7 +167,6 @@ void main() { Directory( p.join(runCommand.shorebirdEnginePath, 'engine'), ).createSync(recursive: true); - when(() => auth.currentSession).thenReturn(session); final progress = _MockProgress(); when(() => logger.progress(any())).thenReturn(progress); diff --git a/packages/shorebird_code_push_client/example/main.dart b/packages/shorebird_code_push_client/example/main.dart index 3b69ec425..5664acbea 100644 --- a/packages/shorebird_code_push_client/example/main.dart +++ b/packages/shorebird_code_push_client/example/main.dart @@ -3,7 +3,7 @@ import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; Future main() async { - final client = CodePushClient(apiKey: ''); + final client = CodePushClient(); // Download the latest stable engine revision. final engine = await client.downloadEngine( diff --git a/packages/shorebird_code_push_client/lib/src/code_push_client.dart b/packages/shorebird_code_push_client/lib/src/code_push_client.dart index 8a885fe71..757eae0b3 100644 --- a/packages/shorebird_code_push_client/lib/src/code_push_client.dart +++ b/packages/shorebird_code_push_client/lib/src/code_push_client.dart @@ -28,24 +28,19 @@ class CodePushException implements Exception { class CodePushClient { /// {@macro code_push_client} CodePushClient({ - required String apiKey, http.Client? httpClient, Uri? hostedUri, - }) : _apiKey = apiKey, - _httpClient = httpClient ?? http.Client(), + }) : _httpClient = httpClient ?? http.Client(), hostedUri = hostedUri ?? Uri.https('api.shorebird.dev'); /// The default error message to use when an unknown error occurs. static const unknownErrorMessage = 'An unknown error occurred.'; - final String _apiKey; final http.Client _httpClient; /// The hosted uri for the Shorebird CodePush API. final Uri hostedUri; - Map get _apiKeyHeader => {'x-api-key': _apiKey}; - /// Create a new artifact for a specific [patchId]. Future createPatchArtifact({ required String artifactPath, @@ -66,7 +61,6 @@ class CodePushClient { 'hash': hash, 'size': '${file.length}', }); - request.headers.addAll(_apiKeyHeader); final response = await _httpClient.send(request); final body = await response.stream.bytesToString(); @@ -95,7 +89,6 @@ class CodePushClient { 'hash': hash, 'size': '${file.length}', }); - request.headers.addAll(_apiKeyHeader); final response = await _httpClient.send(request); final body = await response.stream.bytesToString(); @@ -109,7 +102,6 @@ class CodePushClient { Future createApp({required String displayName}) async { final response = await _httpClient.post( Uri.parse('$hostedUri/api/v1/apps'), - headers: _apiKeyHeader, body: json.encode({'display_name': displayName}), ); @@ -127,7 +119,6 @@ class CodePushClient { }) async { final response = await _httpClient.post( Uri.parse('$hostedUri/api/v1/channels'), - headers: _apiKeyHeader, body: json.encode({'app_id': appId, 'channel': channel}), ); @@ -142,7 +133,6 @@ class CodePushClient { Future createPatch({required int releaseId}) async { final response = await _httpClient.post( Uri.parse('$hostedUri/api/v1/patches'), - headers: _apiKeyHeader, body: json.encode({'release_id': releaseId}), ); @@ -162,7 +152,6 @@ class CodePushClient { }) async { final response = await _httpClient.post( Uri.parse('$hostedUri/api/v1/releases'), - headers: _apiKeyHeader, body: json.encode({ 'app_id': appId, 'version': version, @@ -181,7 +170,6 @@ class CodePushClient { Future deleteApp({required String appId}) async { final response = await _httpClient.delete( Uri.parse('$hostedUri/api/v1/apps/$appId'), - headers: _apiKeyHeader, ); if (response.statusCode != HttpStatus.noContent) { @@ -195,7 +183,6 @@ class CodePushClient { 'GET', Uri.parse('$hostedUri/api/v1/engines/$revision'), ); - request.headers.addAll(_apiKeyHeader); final response = await _httpClient.send(request); @@ -208,10 +195,7 @@ class CodePushClient { /// List all apps for the current account. Future> getApps() async { - final response = await _httpClient.get( - Uri.parse('$hostedUri/api/v1/apps'), - headers: _apiKeyHeader, - ); + final response = await _httpClient.get(Uri.parse('$hostedUri/api/v1/apps')); if (response.statusCode != HttpStatus.ok) { throw _parseErrorResponse(response.body); @@ -229,7 +213,6 @@ class CodePushClient { Uri.parse('$hostedUri/api/v1/channels').replace( queryParameters: {'appId': appId}, ), - headers: _apiKeyHeader, ); if (response.statusCode != HttpStatus.ok) { @@ -248,7 +231,6 @@ class CodePushClient { Uri.parse('$hostedUri/api/v1/releases').replace( queryParameters: {'appId': appId}, ), - headers: _apiKeyHeader, ); if (response.statusCode != HttpStatus.ok) { @@ -274,7 +256,6 @@ class CodePushClient { 'platform': platform, }, ), - headers: _apiKeyHeader, ); if (response.statusCode != HttpStatus.ok) { @@ -292,7 +273,6 @@ class CodePushClient { }) async { final response = await _httpClient.post( Uri.parse('$hostedUri/api/v1/patches/promote'), - headers: _apiKeyHeader, body: json.encode({'patch_id': patchId, 'channel_id': channelId}), ); diff --git a/packages/shorebird_code_push_client/test/src/code_push_client_test.dart b/packages/shorebird_code_push_client/test/src/code_push_client_test.dart index 867a9f3f9..8ab29b214 100644 --- a/packages/shorebird_code_push_client/test/src/code_push_client_test.dart +++ b/packages/shorebird_code_push_client/test/src/code_push_client_test.dart @@ -14,7 +14,6 @@ class _FakeBaseRequest extends Fake implements http.BaseRequest {} void main() { group('CodePushClient', () { - const apiKey = 'api-key'; const appId = 'app-id'; const displayName = 'shorebird-example'; const errorResponse = ErrorResponse( @@ -33,14 +32,11 @@ void main() { setUp(() { httpClient = _MockHttpClient(); - codePushClient = CodePushClient( - apiKey: apiKey, - httpClient: httpClient, - ); + codePushClient = CodePushClient(httpClient: httpClient); }); test('can be instantiated', () { - expect(CodePushClient(apiKey: apiKey), isNotNull); + expect(CodePushClient(), isNotNull); }); group('CodePushException', () {