diff --git a/packages/shorebird_cli/lib/src/commands/patch/android_patcher.dart b/packages/shorebird_cli/lib/src/commands/patch/android_patcher.dart index 453994ef..50c06ac7 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/android_patcher.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/android_patcher.dart @@ -11,7 +11,6 @@ import 'package:shorebird_cli/src/commands/patch/patcher.dart'; import 'package:shorebird_cli/src/common_arguments.dart'; import 'package:shorebird_cli/src/doctor.dart'; import 'package:shorebird_cli/src/extensions/arg_results.dart'; -import 'package:shorebird_cli/src/extensions/file.dart'; import 'package:shorebird_cli/src/logger.dart'; import 'package:shorebird_cli/src/patch_diff_checker.dart'; import 'package:shorebird_cli/src/platform.dart'; @@ -35,11 +34,6 @@ class AndroidPatcher extends Patcher { required super.target, }); - @override - Future assertArgsAreValid() async { - argResults.file(CommonArguments.privateKeyArgName)?.assertExists(); - } - @override ReleaseType get releaseType => ReleaseType.android; @@ -74,6 +68,7 @@ class AndroidPatcher extends Patcher { flavor: flavor, target: target, args: argResults.forwardedArgs, + base64PublicKey: argResults.encodedPublicKey, ); buildProgress.complete(); } on ArtifactBuildException catch (error) { @@ -158,7 +153,9 @@ Looked in: final patchArtifact = File(patchArtifactPath); final hash = sha256.convert(await patchArtifact.readAsBytes()).toString(); - final privateKeyFile = argResults.file(CommonArguments.privateKeyArgName); + final privateKeyFile = argResults.file( + CommonArguments.privateKeyArg.name, + ); final hashSignature = privateKeyFile != null ? codeSigner.sign( message: hash, diff --git a/packages/shorebird_cli/lib/src/commands/patch/ios_framework_patcher.dart b/packages/shorebird_cli/lib/src/commands/patch/ios_framework_patcher.dart index be0117ad..604b904a 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/ios_framework_patcher.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/ios_framework_patcher.dart @@ -70,6 +70,7 @@ class IosFrameworkPatcher extends Patcher { @override Future assertArgsAreValid() async { + await super.assertArgsAreValid(); if (!argResults.wasParsed('release-version')) { logger.err('Missing required argument: --release-version'); exit(ExitCode.usage.code); diff --git a/packages/shorebird_cli/lib/src/commands/patch/ios_patcher.dart b/packages/shorebird_cli/lib/src/commands/patch/ios_patcher.dart index e1d52750..a5eb03a8 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/ios_patcher.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/ios_patcher.dart @@ -100,6 +100,7 @@ class IosPatcher extends Patcher { flavor: flavor, target: target, args: argResults.forwardedArgs, + base64PublicKey: argResults.encodedPublicKey, ); } on ProcessException catch (error) { buildProgress.fail('Failed to build: ${error.message}'); @@ -213,7 +214,7 @@ class IosPatcher extends Patcher { } final patchFileSize = patchFile.statSync().size; - final privateKeyFile = argResults.file(CommonArguments.privateKeyArgName); + final privateKeyFile = argResults.file(CommonArguments.privateKeyArg.name); final hash = sha256.convert(patchBuildFile.readAsBytesSync()).toString(); final hashSignature = privateKeyFile != null ? codeSigner.sign( diff --git a/packages/shorebird_cli/lib/src/commands/patch/patch_command.dart b/packages/shorebird_cli/lib/src/commands/patch/patch_command.dart index 8d6f4f94..ee9d269b 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/patch_command.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/patch_command.dart @@ -95,11 +95,14 @@ of the iOS app that is using this module.''', help: 'Validate but do not upload the patch.', ) ..addOption( - CommonArguments.privateKeyArgName, + CommonArguments.privateKeyArg.name, hide: true, - help: ''' -The path for a private key file that will be used to sign the patch artifact. -''', + help: CommonArguments.privateKeyArg.description, + ) + ..addOption( + CommonArguments.publicKeyArg.name, + hide: true, + help: CommonArguments.publicKeyArg.description, ); } diff --git a/packages/shorebird_cli/lib/src/commands/patch/patcher.dart b/packages/shorebird_cli/lib/src/commands/patch/patcher.dart index 3c361762..8cb9c5e8 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/patcher.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/patcher.dart @@ -2,9 +2,11 @@ import 'dart:io'; import 'package:args/args.dart'; import 'package:mason_logger/mason_logger.dart'; +import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:shorebird_cli/src/archive_analysis/archive_differ.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; +import 'package:shorebird_cli/src/extensions/arg_results.dart'; import 'package:shorebird_cli/src/patch_diff_checker.dart'; import 'package:shorebird_cli/src/platform/platform.dart'; import 'package:shorebird_cli/src/release_type.dart'; @@ -66,8 +68,14 @@ https://docs.shorebird.dev/status#link-percentage-ios /// Asserts that the command can be run. Future assertPreconditions(); + @mustCallSuper + /// Asserts that the combination arguments passed to the command are valid. - Future assertArgsAreValid() async {} + Future assertArgsAreValid() async { + // TODO(erickzanardo): Move this call to the patch_command so we don't need + // to required sub classes to call super. + argResults.assertAbsentOrValidKeyPair(); + } /// Builds the release artifacts for the given platform. Returns the "primary" /// artifact for the platform (e.g. the AAB for Android, the IPA for iOS). diff --git a/packages/shorebird_cli/lib/src/commands/release/release_command.dart b/packages/shorebird_cli/lib/src/commands/release/release_command.dart index 67d21b51..19bf4286 100644 --- a/packages/shorebird_cli/lib/src/commands/release/release_command.dart +++ b/packages/shorebird_cli/lib/src/commands/release/release_command.dart @@ -117,11 +117,9 @@ of the iOS app that is using this module.''', allowed: Arch.values.map((arch) => arch.targetPlatformCliArg), ) ..addOption( - CommonArguments.publicKeyArgName, + CommonArguments.publicKeyArg.name, hide: true, - help: ''' -The path for a public key file that will be used to validate patch signatures. -''', + help: CommonArguments.publicKeyArg.description, ); } diff --git a/packages/shorebird_cli/lib/src/common_arguments.dart b/packages/shorebird_cli/lib/src/common_arguments.dart index 15669ca9..4ae02e52 100644 --- a/packages/shorebird_cli/lib/src/common_arguments.dart +++ b/packages/shorebird_cli/lib/src/common_arguments.dart @@ -1,6 +1,33 @@ +/// {@template argument_describer} +/// A class that describes an argument from a command/sub command. +/// {@endtemplate} +class ArgumentDescriber { + const ArgumentDescriber({ + required this.name, + required this.description, + }); + + /// Argument name as how the user writes it. + final String name; + + /// Argument description that will be shown in the help of the command. + final String description; +} + /// A class that houses the name of arguments that are shared between different /// commands and layers. class CommonArguments { - static const String publicKeyArgName = 'public-key-path'; - static const String privateKeyArgName = 'private-key-path'; + static const publicKeyArg = ArgumentDescriber( + name: 'public-key-path', + description: ''' +The path for a public key file that will be used to validate patch signatures. +''', + ); + + static const privateKeyArg = ArgumentDescriber( + name: 'private-key-path', + description: ''' +The path for a private key file that will be used to sign the patch artifact. +''', + ); } diff --git a/packages/shorebird_cli/lib/src/extensions/arg_results.dart b/packages/shorebird_cli/lib/src/extensions/arg_results.dart index 43fe380b..86683cc0 100644 --- a/packages/shorebird_cli/lib/src/extensions/arg_results.dart +++ b/packages/shorebird_cli/lib/src/extensions/arg_results.dart @@ -1,10 +1,12 @@ -import 'dart:io'; - import 'package:args/args.dart'; import 'package:collection/collection.dart'; +import 'package:mason_logger/mason_logger.dart'; import 'package:shorebird_cli/src/code_signer.dart'; import 'package:shorebird_cli/src/common_arguments.dart'; import 'package:shorebird_cli/src/extensions/file.dart'; +import 'package:shorebird_cli/src/logger.dart'; + +import 'package:shorebird_cli/src/third_party/flutter_tools/lib/src/base/io.dart'; extension OptionFinder on ArgResults { /// // Detects flags even when passed to underlying commands via a `--` @@ -49,12 +51,33 @@ extension CodeSign on ArgResults { /// Asserts that either there is no public key argument /// or that the path received exists. void assertAbsentOrValidPublicKey() { - file(CommonArguments.publicKeyArgName)?.assertExists(); + file(CommonArguments.publicKeyArg.name)?.assertExists(); + } + + /// Asserts that either there is no private key argument + /// or that the path received exists. + void assertAbsentOrValidPrivateKey() { + file(CommonArguments.privateKeyArg.name)?.assertExists(); + } + + /// Asserts that both public and private keys are either absent or + /// when provided, that both of them are pointing to existing files. + void assertAbsentOrValidKeyPair() { + final publicKeyWasParsed = wasParsed(CommonArguments.publicKeyArg.name); + final privateKeyWasParsed = wasParsed(CommonArguments.privateKeyArg.name); + + if (publicKeyWasParsed == privateKeyWasParsed) { + assertAbsentOrValidPublicKey(); + assertAbsentOrValidPrivateKey(); + } else { + logger.err('Both public and private keys must be provided or absent.'); + exit(ExitCode.usage.code); + } } /// Read the public key file and encode it to base64 if any. String? get encodedPublicKey { - final publicKeyFile = file(CommonArguments.publicKeyArgName); + final publicKeyFile = file(CommonArguments.publicKeyArg.name); return publicKeyFile != null ? codeSigner.base64PublicKey(publicKeyFile) diff --git a/packages/shorebird_cli/test/src/commands/patch/android_patcher_test.dart b/packages/shorebird_cli/test/src/commands/patch/android_patcher_test.dart index f08a9ea0..e4398ed0 100644 --- a/packages/shorebird_cli/test/src/commands/patch/android_patcher_test.dart +++ b/packages/shorebird_cli/test/src/commands/patch/android_patcher_test.dart @@ -29,6 +29,7 @@ import 'package:shorebird_cli/src/version.dart'; import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; import 'package:test/test.dart'; +import '../../helpers.dart'; import '../../matchers.dart'; import '../../mocks.dart'; @@ -158,12 +159,6 @@ void main() { }); }); - group('assertArgsAreValid', () { - test('does nothing', () async { - await expectLater(patcher.assertArgsAreValid(), completes); - }); - }); - group('assertPreconditions', () { setUp(() { when(() => doctor.androidCommandValidators) @@ -246,6 +241,7 @@ void main() { target: any(named: 'target'), targetPlatforms: any(named: 'targetPlatforms'), args: any(named: 'args'), + base64PublicKey: any(named: 'base64PublicKey'), ), ).thenAnswer((_) async => aabFile); }); @@ -319,6 +315,38 @@ Looked in: }); }); + group('when the key pair is provided', () { + setUp(() { + when(() => codeSigner.base64PublicKey(any())) + .thenReturn('public_key_encoded'); + }); + + test('calls buildIpa with the provided key', () async { + when(() => argResults.wasParsed(CommonArguments.publicKeyArg.name)) + .thenReturn(true); + + final key = createTempFile('public.der') + ..writeAsStringSync('public_key'); + + when(() => argResults[CommonArguments.publicKeyArg.name]) + .thenReturn(key.path); + when(() => argResults[CommonArguments.publicKeyArg.name]) + .thenReturn(key.path); + await runWithOverrides( + patcher.buildPatchArtifact, + ); + + verify( + () => artifactBuilder.buildAppBundle( + args: any(named: 'args'), + flavor: any(named: 'flavor'), + target: any(named: 'target'), + base64PublicKey: 'public_key_encoded', + ), + ).called(1); + }); + }); + test('returns the aab file', () async { final result = await runWithOverrides(patcher.buildPatchArtifact); expect(result, equals(aabFile)); @@ -467,7 +495,7 @@ Looked in: ), )..createSync(); - when(() => argResults[CommonArguments.privateKeyArgName]) + when(() => argResults[CommonArguments.privateKeyArg.name]) .thenReturn(privateKey.path); when( diff --git a/packages/shorebird_cli/test/src/commands/patch/ios_framework_patcher_test.dart b/packages/shorebird_cli/test/src/commands/patch/ios_framework_patcher_test.dart index c76c8e3d..4530e6d1 100644 --- a/packages/shorebird_cli/test/src/commands/patch/ios_framework_patcher_test.dart +++ b/packages/shorebird_cli/test/src/commands/patch/ios_framework_patcher_test.dart @@ -9,6 +9,7 @@ import 'package:shorebird_cli/src/artifact_builder.dart'; import 'package:shorebird_cli/src/artifact_manager.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; import 'package:shorebird_cli/src/commands/patch/patch.dart'; +import 'package:shorebird_cli/src/common_arguments.dart'; import 'package:shorebird_cli/src/doctor.dart'; import 'package:shorebird_cli/src/engine_config.dart'; import 'package:shorebird_cli/src/executables/aot_tools.dart'; @@ -115,6 +116,10 @@ void main() { when(() => argResults['build-number']).thenReturn('1.0'); when(() => argResults.rest).thenReturn([]); + when(() => argResults.wasParsed(CommonArguments.privateKeyArg.name)) + .thenReturn(false); + when(() => argResults.wasParsed(CommonArguments.publicKeyArg.name)) + .thenReturn(false); when(() => logger.progress(any())).thenReturn(progress); diff --git a/packages/shorebird_cli/test/src/commands/patch/ios_patcher_test.dart b/packages/shorebird_cli/test/src/commands/patch/ios_patcher_test.dart index 77776f17..e95e2e12 100644 --- a/packages/shorebird_cli/test/src/commands/patch/ios_patcher_test.dart +++ b/packages/shorebird_cli/test/src/commands/patch/ios_patcher_test.dart @@ -33,6 +33,7 @@ import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; import 'package:test/test.dart'; import '../../fakes.dart'; +import '../../helpers.dart'; import '../../matchers.dart'; import '../../mocks.dart'; @@ -143,12 +144,6 @@ void main() { ); }); - group('assertArgsAreValid', () { - test('has no specific validations', () { - expect(patcher.assertArgsAreValid, returnsNormally); - }); - }); - group('archiveDiffer', () { test('is an IosArchiveDiffer', () { expect(patcher.archiveDiffer, isA()); @@ -389,6 +384,7 @@ void main() { args: any(named: 'args'), flavor: any(named: 'flavor'), target: any(named: 'target'), + base64PublicKey: any(named: 'base64PublicKey'), ), ).thenAnswer((_) async {}); when(() => artifactManager.getXcarchiveDirectory()).thenReturn( @@ -436,6 +432,41 @@ void main() { }); }); + group('when the key pair is provided', () { + setUp(() { + when(() => codeSigner.base64PublicKey(any())) + .thenReturn('public_key_encoded'); + }); + + test('calls the buildIpa passing the key', () async { + when( + () => argResults.wasParsed(CommonArguments.publicKeyArg.name), + ).thenReturn(true); + + final key = createTempFile('public.pem') + ..writeAsStringSync('public_key'); + + when(() => argResults[CommonArguments.publicKeyArg.name]) + .thenReturn(key.path); + when(() => argResults[CommonArguments.publicKeyArg.name]) + .thenReturn(key.path); + await runWithOverrides( + patcher.buildPatchArtifact, + ); + + verify( + () => artifactBuilder.buildIpa( + exportOptionsPlist: any(named: 'exportOptionsPlist'), + codesign: any(named: 'codesign'), + args: any(named: 'args'), + flavor: any(named: 'flavor'), + target: any(named: 'target'), + base64PublicKey: 'public_key_encoded', + ), + ).called(1); + }); + }); + test('returns xcarchive zip', () async { final artifact = await runWithOverrides(patcher.buildPatchArtifact); expect(p.basename(artifact.path), endsWith('.zip')); @@ -941,7 +972,7 @@ void main() { }); }); - group('when a private key is provided', () { + group('when code signing the patch', () { setUp(() { final privateKey = File( p.join( @@ -950,7 +981,7 @@ void main() { ), )..createSync(); - when(() => argResults[CommonArguments.privateKeyArgName]) + when(() => argResults[CommonArguments.privateKeyArg.name]) .thenReturn(privateKey.path); when( diff --git a/packages/shorebird_cli/test/src/commands/patch/patcher_test.dart b/packages/shorebird_cli/test/src/commands/patch/patcher_test.dart index 753e1f58..fa8a2974 100644 --- a/packages/shorebird_cli/test/src/commands/patch/patcher_test.dart +++ b/packages/shorebird_cli/test/src/commands/patch/patcher_test.dart @@ -1,14 +1,21 @@ -import 'dart:io'; - +import 'package:args/args.dart'; +import 'package:mason_logger/mason_logger.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:scoped_deps/scoped_deps.dart'; import 'package:shorebird_cli/src/archive_analysis/archive_differ.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; import 'package:shorebird_cli/src/commands/commands.dart'; +import 'package:shorebird_cli/src/common_arguments.dart'; +import 'package:shorebird_cli/src/logger.dart'; import 'package:shorebird_cli/src/patch_diff_checker.dart'; import 'package:shorebird_cli/src/platform/platform.dart'; import 'package:shorebird_cli/src/release_type.dart'; +import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart'; import 'package:shorebird_code_push_protocol/src/models/create_patch_metadata.dart'; import 'package:test/test.dart'; +import '../../helpers.dart'; +import '../../matchers.dart'; import '../../mocks.dart'; void main() { @@ -25,6 +32,119 @@ void main() { ); }); }); + + group('assertArgsAreValid', () { + late _TestPatcher patcher; + late ArgResults argResults; + late ShorebirdLogger logger; + + R runWithOverrides(R Function() body) { + return runScoped( + body, + values: { + loggerRef.overrideWith(() => logger), + }, + ); + } + + setUp(() { + setExitFunctionForTests(); + argResults = MockArgResults(); + logger = MockShorebirdLogger(); + patcher = _TestPatcher( + argResults: argResults, + flavor: null, + target: null, + ); + + when(() => argResults.wasParsed(CommonArguments.publicKeyArg.name)) + .thenReturn(false); + + when(() => argResults.wasParsed(CommonArguments.privateKeyArg.name)) + .thenReturn(false); + }); + + group('when no key pair is provided', () { + test('is valid', () { + expect( + runWithOverrides(patcher.assertArgsAreValid), + completes, + ); + }); + }); + + group( + 'when given existing private and public key files', + () { + test('is valid', () async { + when( + () => argResults.wasParsed(CommonArguments.privateKeyArg.name), + ).thenReturn(true); + when(() => argResults.wasParsed(CommonArguments.publicKeyArg.name)) + .thenReturn(true); + when(() => argResults[CommonArguments.privateKeyArg.name]) + .thenReturn(createTempFile('private.pem').path); + when(() => argResults[CommonArguments.publicKeyArg.name]) + .thenReturn(createTempFile('public.pem').path); + + expect( + runWithOverrides(patcher.assertArgsAreValid), + completes, + ); + }); + }, + ); + + group( + 'when given an existing private key and nonexistent public key', + () { + test('logs error and exits with usage code', () async { + when( + () => argResults.wasParsed(CommonArguments.privateKeyArg.name), + ).thenReturn(true); + when(() => argResults.wasParsed(CommonArguments.publicKeyArg.name)) + .thenReturn(false); + when(() => argResults[CommonArguments.privateKeyArg.name]) + .thenReturn(createTempFile('private.pem').path); + + await expectLater( + () => runWithOverrides(patcher.assertArgsAreValid), + exitsWithCode(ExitCode.usage), + ); + verify( + () => logger.err( + 'Both public and private keys must be provided or absent.', + ), + ).called(1); + }); + }, + ); + + group( + 'when given an existing public key and nonexistent private key', + () { + test('fails and logs the err', () async { + when( + () => argResults.wasParsed(CommonArguments.privateKeyArg.name), + ).thenReturn(false); + when(() => argResults.wasParsed(CommonArguments.publicKeyArg.name)) + .thenReturn(true); + when(() => argResults[CommonArguments.publicKeyArg.name]) + .thenReturn(createTempFile('public.pem').path); + + await expectLater( + () => runWithOverrides(patcher.assertArgsAreValid), + exitsWithCode(ExitCode.usage), + ); + verify( + () => logger.err( + 'Both public and private keys must be provided or absent.', + ), + ).called(1); + }); + }, + ); + }); }); } diff --git a/packages/shorebird_cli/test/src/commands/release/android_releaser_test.dart b/packages/shorebird_cli/test/src/commands/release/android_releaser_test.dart index bfa33788..80403779 100644 --- a/packages/shorebird_cli/test/src/commands/release/android_releaser_test.dart +++ b/packages/shorebird_cli/test/src/commands/release/android_releaser_test.dart @@ -227,7 +227,7 @@ void main() { 'public-key.pem', ), )..writeAsStringSync('public key'); - when(() => argResults[CommonArguments.publicKeyArgName]) + when(() => argResults[CommonArguments.publicKeyArg.name]) .thenReturn(publicKeyFile.path); }); @@ -242,7 +242,7 @@ void main() { group('when a public key is provided but does not exist', () { setUp(() { when(() => argResults['artifact']).thenReturn('apk'); - when(() => argResults[CommonArguments.publicKeyArgName]) + when(() => argResults[CommonArguments.publicKeyArg.name]) .thenReturn('non-existing-key.pem'); }); @@ -434,7 +434,7 @@ void main() { 'patch-signing-public-key.pem', ), )..createSync(recursive: true); - when(() => argResults[CommonArguments.publicKeyArgName]) + when(() => argResults[CommonArguments.publicKeyArg.name]) .thenReturn(patchSigningPublicKeyFile.path); when( diff --git a/packages/shorebird_cli/test/src/commands/release/ios_releaser_test.dart b/packages/shorebird_cli/test/src/commands/release/ios_releaser_test.dart index 262f9887..4a509c96 100644 --- a/packages/shorebird_cli/test/src/commands/release/ios_releaser_test.dart +++ b/packages/shorebird_cli/test/src/commands/release/ios_releaser_test.dart @@ -258,7 +258,7 @@ void main() { 'public-key.pem', ), )..writeAsStringSync('public key'); - when(() => argResults[CommonArguments.publicKeyArgName]) + when(() => argResults[CommonArguments.publicKeyArg.name]) .thenReturn(publicKeyFile.path); }); @@ -272,7 +272,7 @@ void main() { group('when the provided public key is a nonexistent file', () { setUp(() { - when(() => argResults[CommonArguments.publicKeyArgName]) + when(() => argResults[CommonArguments.publicKeyArg.name]) .thenReturn('non-existing-key.pem'); }); @@ -339,7 +339,7 @@ void main() { 'patch-signing-public-key.pem', ), )..createSync(recursive: true); - when(() => argResults[CommonArguments.publicKeyArgName]) + when(() => argResults[CommonArguments.publicKeyArg.name]) .thenReturn(patchSigningPublicKeyFile.path); when( diff --git a/packages/shorebird_cli/test/src/commands/release/release_command_test.dart b/packages/shorebird_cli/test/src/commands/release/release_command_test.dart index 5f351998..e38a044d 100644 --- a/packages/shorebird_cli/test/src/commands/release/release_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/release/release_command_test.dart @@ -501,7 +501,7 @@ $exception''', keyName, ), )..writeAsStringSync('KEY'); - when(() => argResults[CommonArguments.publicKeyArgName]) + when(() => argResults[CommonArguments.publicKeyArg.name]) .thenReturn(file.path); }); diff --git a/packages/shorebird_cli/test/src/helpers.dart b/packages/shorebird_cli/test/src/helpers.dart new file mode 100644 index 00000000..e313320c --- /dev/null +++ b/packages/shorebird_cli/test/src/helpers.dart @@ -0,0 +1,11 @@ +import 'dart:io'; +import 'package:path/path.dart' as p; + +File createTempFile(String name) { + return File( + p.join( + Directory.systemTemp.createTempSync().path, + name, + ), + )..createSync(); +}