feat(shorebird_cli): add --public-key-cmd and --sign-cmd support (#3605)
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
# Run all tests (must run from packages/ directory to avoid cache conflicts)
|
||||
cd packages && very_good test -r
|
||||
|
||||
# Run tests for a single package (can use -r failures-only to reduce output)
|
||||
dart test packages/shorebird_cli
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
Dart monorepo. Main package is `shorebird_cli`.
|
||||
|
||||
**Platform-specific operations** use the Releaser/Patcher pattern:
|
||||
- `commands/release/` - `Releaser` base class with platform implementations
|
||||
- `commands/patch/` - `Patcher` base class with platform implementations
|
||||
|
||||
**Dependency injection** uses `scoped_deps` with zone-based refs (see any `*Ref` variable).
|
||||
|
||||
## Code Style
|
||||
|
||||
- PR titles must follow semantic commit format (enforced in CI)
|
||||
- CSpell: use inline `// cspell:words` for 1-2 files; add to global config for more
|
||||
- Prefer new commits over amending in PRs - history gets squashed anyway
|
||||
@@ -6,6 +6,7 @@ import 'dart:typed_data';
|
||||
import 'package:pem/pem.dart';
|
||||
import 'package:pointycastle/pointycastle.dart';
|
||||
import 'package:scoped_deps/scoped_deps.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_process.dart';
|
||||
|
||||
/// A reference to a [CodeSigner] instance.
|
||||
final codeSignerRef = create(CodeSigner.new);
|
||||
@@ -53,8 +54,13 @@ class CodeSigner {
|
||||
/// simply the modulus and exponent of the public key, without information
|
||||
/// about the algorithm or or ASN1 object type identifier.
|
||||
String base64PublicKey(File publicKeyPemFile) {
|
||||
return base64PublicKeyFromPem(publicKeyPemFile.readAsStringSync());
|
||||
}
|
||||
|
||||
/// Extracts the base64 encoded DER from a PEM-encoded public key string.
|
||||
String base64PublicKeyFromPem(String publicKeyPem) {
|
||||
final publicKey = _RSAPublicKeyFromBytes.rsaPublicKeyFromBytes(
|
||||
_pemBytes(pemFile: publicKeyPemFile, type: PemLabel.publicKey),
|
||||
PemCodec(PemLabel.publicKey).decode(publicKeyPem),
|
||||
);
|
||||
|
||||
final publicKeySeq = ASN1Sequence()
|
||||
@@ -64,11 +70,104 @@ class CodeSigner {
|
||||
return base64.encode(publicKeySeq.encodedBytes!);
|
||||
}
|
||||
|
||||
/// Reads a PEM file containing a key of type [type] and returns its contents
|
||||
/// as bytes.
|
||||
List<int> _pemBytes({required File pemFile, required PemLabel type}) {
|
||||
final privateKeyString = pemFile.readAsStringSync();
|
||||
return PemCodec(type).decode(privateKeyString);
|
||||
/// Runs a command and returns its stdout, expected to be a PEM public key.
|
||||
Future<String> runPublicKeyCmd(String command) async {
|
||||
final result = await process.run(
|
||||
'sh',
|
||||
['-c', command],
|
||||
);
|
||||
|
||||
if (result.exitCode != 0) {
|
||||
throw ProcessException(
|
||||
command,
|
||||
[],
|
||||
'Command failed with exit code ${result.exitCode}: ${result.stderr}',
|
||||
result.exitCode,
|
||||
);
|
||||
}
|
||||
|
||||
final output = '${result.stdout}'.trim();
|
||||
if (!output.contains('-----BEGIN') || !output.contains('PUBLIC KEY')) {
|
||||
if (output.isEmpty) {
|
||||
throw const FormatException(
|
||||
'Command produced no output. '
|
||||
'Expected a PEM-encoded public key.',
|
||||
);
|
||||
}
|
||||
final preview = output.length > 100
|
||||
? '${output.substring(0, 100)}...'
|
||||
: output;
|
||||
throw FormatException(
|
||||
'Command output does not appear to be a PEM-encoded public key: '
|
||||
'$preview',
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/// Signs data by piping it to a command's stdin and reading the base64
|
||||
/// signature from stdout.
|
||||
Future<String> signWithCmd({
|
||||
required String data,
|
||||
required String command,
|
||||
}) async {
|
||||
final proc = await process.start(
|
||||
'sh',
|
||||
['-c', command],
|
||||
);
|
||||
|
||||
// Write data to stdin and close it
|
||||
proc.stdin.write(data);
|
||||
await proc.stdin.close();
|
||||
|
||||
// Read stdout and stderr concurrently to avoid potential deadlock
|
||||
final results = await Future.wait([
|
||||
proc.stdout.transform(utf8.decoder).join(),
|
||||
proc.stderr.transform(utf8.decoder).join(),
|
||||
]);
|
||||
final stdout = results[0];
|
||||
final stderr = results[1];
|
||||
final exitCode = await proc.exitCode;
|
||||
|
||||
if (exitCode != 0) {
|
||||
throw ProcessException(
|
||||
command,
|
||||
[],
|
||||
'Sign command failed with exit code $exitCode: $stderr',
|
||||
exitCode,
|
||||
);
|
||||
}
|
||||
|
||||
final signature = stdout.trim();
|
||||
if (signature.isEmpty) {
|
||||
throw const FormatException('Sign command produced no output');
|
||||
}
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
/// Verifies a signature against a message using a PEM-encoded public key.
|
||||
bool verify({
|
||||
required String message,
|
||||
required String signature,
|
||||
required String publicKeyPem,
|
||||
}) {
|
||||
final publicKey = _RSAPublicKeyFromBytes.rsaPublicKeyFromBytes(
|
||||
PemCodec(PemLabel.publicKey).decode(publicKeyPem),
|
||||
);
|
||||
|
||||
final signer = Signer('SHA-256/RSA')
|
||||
..init(false, PublicKeyParameter<RSAPublicKey>(publicKey));
|
||||
|
||||
try {
|
||||
return signer.verifySignature(
|
||||
utf8.encode(message),
|
||||
RSASignature(base64.decode(signature)),
|
||||
);
|
||||
} on Exception {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a PEM-encoded private key string and returns the key bytes along
|
||||
|
||||
@@ -9,9 +9,7 @@ import 'package:shorebird_cli/src/archive_analysis/android_archive_differ.dart';
|
||||
import 'package:shorebird_cli/src/artifact_builder/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/code_signer.dart';
|
||||
import 'package:shorebird_cli/src/commands/patch/patch.dart';
|
||||
import 'package:shorebird_cli/src/common_arguments.dart';
|
||||
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
||||
import 'package:shorebird_cli/src/logging/logging.dart';
|
||||
import 'package:shorebird_cli/src/patch_diff_checker.dart';
|
||||
@@ -143,13 +141,7 @@ class AarPatcher extends Patcher {
|
||||
logger.detail('Creating artifact for $artifactPath');
|
||||
final patchArtifact = File(artifactPath);
|
||||
final hash = sha256.convert(await patchArtifact.readAsBytes()).toString();
|
||||
|
||||
final privateKeyFile = argResults.file(
|
||||
CommonArguments.privateKeyArg.name,
|
||||
);
|
||||
final hashSignature = privateKeyFile != null
|
||||
? codeSigner.sign(message: hash, privateKeyPemFile: privateKeyFile)
|
||||
: null;
|
||||
final hashSignature = await signHash(hash);
|
||||
|
||||
try {
|
||||
final diffPath = await artifactManager.createDiff(
|
||||
|
||||
@@ -9,9 +9,7 @@ import 'package:shorebird_cli/src/archive_analysis/android_archive_differ.dart';
|
||||
import 'package:shorebird_cli/src/artifact_builder/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/code_signer.dart';
|
||||
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/logging/logging.dart';
|
||||
@@ -189,13 +187,7 @@ Please refer to ${link(uri: Uri.parse('https://github.com/shorebirdtech/shorebir
|
||||
logger.detail('Creating artifact for $patchArtifactPath');
|
||||
final patchArtifact = File(patchArtifactPath);
|
||||
final hash = sha256.convert(await patchArtifact.readAsBytes()).toString();
|
||||
|
||||
final privateKeyFile = argResults.file(
|
||||
CommonArguments.privateKeyArg.name,
|
||||
);
|
||||
final hashSignature = privateKeyFile != null
|
||||
? codeSigner.sign(message: hash, privateKeyPemFile: privateKeyFile)
|
||||
: null;
|
||||
final hashSignature = await signHash(hash);
|
||||
|
||||
try {
|
||||
final diffPath = await artifactManager.createDiff(
|
||||
|
||||
@@ -10,9 +10,7 @@ import 'package:shorebird_cli/src/archive_analysis/apple_archive_differ.dart';
|
||||
import 'package:shorebird_cli/src/artifact_builder/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/code_signer.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/executables/aot_tools.dart';
|
||||
import 'package:shorebird_cli/src/executables/xcodebuild.dart';
|
||||
@@ -232,11 +230,8 @@ class IosFrameworkPatcher extends Patcher {
|
||||
}
|
||||
|
||||
final patchFileSize = patchFile.statSync().size;
|
||||
final privateKeyFile = argResults.file(CommonArguments.privateKeyArg.name);
|
||||
final hash = sha256.convert(patchBuildFile.readAsBytesSync()).toString();
|
||||
final hashSignature = privateKeyFile != null
|
||||
? codeSigner.sign(message: hash, privateKeyPemFile: privateKeyFile)
|
||||
: null;
|
||||
final hashSignature = await signHash(hash);
|
||||
|
||||
return {
|
||||
Arch.arm64: PatchArtifactBundle(
|
||||
|
||||
@@ -11,9 +11,7 @@ import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart';
|
||||
import 'package:shorebird_cli/src/artifact_builder/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/code_signer.dart';
|
||||
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/executables/executables.dart';
|
||||
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
||||
@@ -304,11 +302,8 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''');
|
||||
}
|
||||
|
||||
final patchFileSize = patchFile.statSync().size;
|
||||
final privateKeyFile = argResults.file(CommonArguments.privateKeyArg.name);
|
||||
final hash = sha256.convert(patchBuildFile.readAsBytesSync()).toString();
|
||||
final hashSignature = privateKeyFile != null
|
||||
? codeSigner.sign(message: hash, privateKeyPemFile: privateKeyFile)
|
||||
: null;
|
||||
final hashSignature = await signHash(hash);
|
||||
|
||||
return {
|
||||
Arch.arm64: PatchArtifactBundle(
|
||||
|
||||
@@ -8,9 +8,7 @@ import 'package:shorebird_cli/src/archive_analysis/linux_bundle_differ.dart';
|
||||
import 'package:shorebird_cli/src/artifact_builder/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/code_signer.dart';
|
||||
import 'package:shorebird_cli/src/commands/commands.dart';
|
||||
import 'package:shorebird_cli/src/common_arguments.dart';
|
||||
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
||||
import 'package:shorebird_cli/src/logging/logging.dart';
|
||||
import 'package:shorebird_cli/src/patch_diff_checker.dart';
|
||||
@@ -85,10 +83,7 @@ class LinuxPatcher extends Patcher {
|
||||
// build/linux/x64/release/bundle
|
||||
final appSoPath = p.join(tempDir.path, 'lib', 'libapp.so');
|
||||
|
||||
final privateKeyFile = argResults.file(CommonArguments.privateKeyArg.name);
|
||||
final hashSignature = privateKeyFile != null
|
||||
? codeSigner.sign(message: hash, privateKeyPemFile: privateKeyFile)
|
||||
: null;
|
||||
final hashSignature = await signHash(hash);
|
||||
|
||||
final String diffPath;
|
||||
try {
|
||||
|
||||
@@ -8,9 +8,7 @@ import 'package:shorebird_cli/src/archive_analysis/apple_archive_differ.dart';
|
||||
import 'package:shorebird_cli/src/artifact_builder/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/code_signer.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/executables/executables.dart';
|
||||
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
||||
@@ -201,11 +199,8 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''');
|
||||
|
||||
final patchFile = File(patchFilePath);
|
||||
final patchFileSize = patchFile.statSync().size;
|
||||
final privateKeyFile = argResults.file(CommonArguments.privateKeyArg.name);
|
||||
final hash = sha256.convert(patchArtifact.readAsBytesSync()).toString();
|
||||
final hashSignature = privateKeyFile != null
|
||||
? codeSigner.sign(message: hash, privateKeyPemFile: privateKeyFile)
|
||||
: null;
|
||||
final hashSignature = await signHash(hash);
|
||||
|
||||
return PatchArtifactBundle(
|
||||
arch: arch.arch,
|
||||
|
||||
@@ -140,6 +140,14 @@ To target the latest release (e.g. the release that was most recently updated) u
|
||||
CommonArguments.publicKeyArg.name,
|
||||
help: CommonArguments.publicKeyArg.description,
|
||||
)
|
||||
..addOption(
|
||||
CommonArguments.publicKeyCmd.name,
|
||||
help: CommonArguments.publicKeyCmd.description,
|
||||
)
|
||||
..addOption(
|
||||
CommonArguments.signCmd.name,
|
||||
help: CommonArguments.signCmd.description,
|
||||
)
|
||||
..addOption(
|
||||
CommonArguments.splitDebugInfoArg.name,
|
||||
help: CommonArguments.splitDebugInfoArg.description,
|
||||
@@ -287,7 +295,7 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl
|
||||
Future<void> createPatch(Patcher patcher) async {
|
||||
await patcher.assertPreconditions();
|
||||
await patcher.assertArgsAreValid();
|
||||
results.assertAbsentOrValidKeyPair();
|
||||
results.assertAbsentOrValidKeyPairOrCommands();
|
||||
|
||||
try {
|
||||
await shorebirdValidator.validateFlavors(
|
||||
@@ -444,7 +452,9 @@ Building patch with Flutter $flutterVersionString
|
||||
usedIgnoreNativeChangesFlag: allowNativeDiffs,
|
||||
hasNativeChanges: diffStatus.hasNativeChanges,
|
||||
inferredReleaseVersion: inferredReleaseVersion,
|
||||
isSigned: results.wasParsed(CommonArguments.privateKeyArg.name),
|
||||
isSigned:
|
||||
results.wasParsed(CommonArguments.privateKeyArg.name) ||
|
||||
results.wasParsed(CommonArguments.signCmd.name),
|
||||
environment: BuildEnvironmentMetadata(
|
||||
flutterRevision: shorebirdEnv.flutterRevision,
|
||||
operatingSystem: platform.operatingSystem,
|
||||
|
||||
@@ -4,16 +4,19 @@ import 'package:args/args.dart';
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||
import 'package:shorebird_cli/src/code_signer.dart';
|
||||
import 'package:shorebird_cli/src/common_arguments.dart';
|
||||
import 'package:shorebird_cli/src/deployment_track.dart';
|
||||
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
||||
import 'package:shorebird_cli/src/extensions/iterable.dart';
|
||||
import 'package:shorebird_cli/src/logging/logging.dart';
|
||||
import 'package:shorebird_cli/src/metadata/metadata.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/shorebird_documentation.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_env.dart';
|
||||
import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart';
|
||||
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
|
||||
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
|
||||
|
||||
@@ -127,6 +130,90 @@ More info: ${troubleshootingUrl.toLink()}.
|
||||
/// Whether to allow changes in native code (--allow-native-diffs).
|
||||
bool get allowNativeDiffs => argResults['allow-native-diffs'] == true;
|
||||
|
||||
/// Returns a function that signs data, or null if no signing is configured.
|
||||
Future<String> Function(String)? _resolveSigner() {
|
||||
final privateKeyFile = argResults.file(CommonArguments.privateKeyArg.name);
|
||||
if (privateKeyFile != null) {
|
||||
return (hash) async =>
|
||||
codeSigner.sign(message: hash, privateKeyPemFile: privateKeyFile);
|
||||
}
|
||||
|
||||
final signCmd = argResults[CommonArguments.signCmd.name] as String?;
|
||||
if (signCmd != null) {
|
||||
return (hash) async {
|
||||
try {
|
||||
return await codeSigner.signWithCmd(data: hash, command: signCmd);
|
||||
} on ProcessException catch (e) {
|
||||
logger.err(
|
||||
'Failed to run --${CommonArguments.signCmd.name}: ${e.message}',
|
||||
);
|
||||
throw ProcessExit(ExitCode.software.code);
|
||||
} on FormatException catch (e) {
|
||||
logger.err(
|
||||
'--${CommonArguments.signCmd.name} produced invalid output: '
|
||||
'${e.message}',
|
||||
);
|
||||
throw ProcessExit(ExitCode.software.code);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns the public key PEM from the configured source, or null.
|
||||
Future<String?> _resolvePublicKeyPem() async {
|
||||
try {
|
||||
return await argResults.resolvePublicKeyPem();
|
||||
} on ProcessException catch (e) {
|
||||
logger.err(
|
||||
'Failed to run '
|
||||
'--${CommonArguments.publicKeyCmd.name}: ${e.message}',
|
||||
);
|
||||
throw ProcessExit(ExitCode.software.code);
|
||||
} on FormatException catch (e) {
|
||||
logger.err(
|
||||
'--${CommonArguments.publicKeyCmd.name} produced invalid output: '
|
||||
'${e.message}',
|
||||
);
|
||||
throw ProcessExit(ExitCode.software.code);
|
||||
}
|
||||
}
|
||||
|
||||
/// Signs a hash using the configured signing method.
|
||||
///
|
||||
/// Returns null if no signing is configured.
|
||||
Future<String?> signHash(String hash) async {
|
||||
final signer = _resolveSigner();
|
||||
if (signer == null) return null;
|
||||
|
||||
final signature = await signer(hash);
|
||||
final publicKeyPem = await _resolvePublicKeyPem();
|
||||
|
||||
if (publicKeyPem == null) {
|
||||
logger.err(
|
||||
'A public key is required for code signing. '
|
||||
'Provide --${CommonArguments.publicKeyArg.name} or '
|
||||
'--${CommonArguments.publicKeyCmd.name}.',
|
||||
);
|
||||
throw ProcessExit(ExitCode.usage.code);
|
||||
}
|
||||
|
||||
if (!codeSigner.verify(
|
||||
message: hash,
|
||||
signature: signature,
|
||||
publicKeyPem: publicKeyPem,
|
||||
)) {
|
||||
logger.err(
|
||||
'Signature verification failed. The signature does not match '
|
||||
'the provided public key.',
|
||||
);
|
||||
throw ProcessExit(ExitCode.software.code);
|
||||
}
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
/// The link percentage for the generated patch artifact if applicable.
|
||||
/// Returns `null` if the platform does not use a linker or if the linking
|
||||
/// step has not yet been run.
|
||||
|
||||
@@ -9,9 +9,7 @@ import 'package:shorebird_cli/src/archive_analysis/windows_archive_differ.dart';
|
||||
import 'package:shorebird_cli/src/artifact_builder/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/code_signer.dart';
|
||||
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/executables/executables.dart';
|
||||
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
||||
@@ -109,10 +107,7 @@ class WindowsPatcher extends Patcher {
|
||||
// build/windows/x64/runner/Release
|
||||
final appSoPath = p.join(tempDir.path, 'data', 'app.so');
|
||||
|
||||
final privateKeyFile = argResults.file(CommonArguments.privateKeyArg.name);
|
||||
final hashSignature = privateKeyFile != null
|
||||
? codeSigner.sign(message: hash, privateKeyPemFile: privateKeyFile)
|
||||
: null;
|
||||
final hashSignature = await signHash(hash);
|
||||
|
||||
final String diffPath;
|
||||
try {
|
||||
|
||||
@@ -74,11 +74,12 @@ class AarReleaser extends Releaser {
|
||||
|
||||
@override
|
||||
Future<FileSystemEntity> buildReleaseArtifacts() async {
|
||||
final base64PublicKey = await getEncodedPublicKey();
|
||||
await artifactBuilder.buildAar(
|
||||
buildNumber: buildNumber,
|
||||
targetPlatforms: architectures,
|
||||
args: argResults.forwardedArgs,
|
||||
base64PublicKey: argResults.encodedPublicKey,
|
||||
base64PublicKey: base64PublicKey,
|
||||
);
|
||||
|
||||
// Copy release AAR to a new directory to avoid overwriting with
|
||||
|
||||
@@ -102,7 +102,7 @@ Please comment and upvote ${link(uri: Uri.parse('https://github.com/shorebirdtec
|
||||
|
||||
@override
|
||||
Future<FileSystemEntity> buildReleaseArtifacts() async {
|
||||
final base64PublicKey = argResults.encodedPublicKey;
|
||||
final base64PublicKey = await getEncodedPublicKey();
|
||||
final aab = await artifactBuilder.buildAppBundle(
|
||||
flavor: flavor,
|
||||
target: target,
|
||||
|
||||
@@ -78,9 +78,10 @@ class IosFrameworkReleaser extends Releaser {
|
||||
shorebirdSupplementDir!.deleteSync(recursive: true);
|
||||
}
|
||||
|
||||
final base64PublicKey = await getEncodedPublicKey();
|
||||
await artifactBuilder.buildIosFramework(
|
||||
args: argResults.forwardedArgs,
|
||||
base64PublicKey: argResults.encodedPublicKey,
|
||||
base64PublicKey: base64PublicKey,
|
||||
);
|
||||
|
||||
// Copy release xcframework to a new directory to avoid overwriting with
|
||||
|
||||
@@ -102,12 +102,13 @@ To change the version of this release, change your app's version in your pubspec
|
||||
shorebirdSupplementDir!.deleteSync(recursive: true);
|
||||
}
|
||||
|
||||
final base64PublicKey = await getEncodedPublicKey();
|
||||
await artifactBuilder.buildIpa(
|
||||
codesign: codesign,
|
||||
flavor: flavor,
|
||||
target: target,
|
||||
args: argResults.forwardedArgs,
|
||||
base64PublicKey: argResults.encodedPublicKey,
|
||||
base64PublicKey: base64PublicKey,
|
||||
);
|
||||
|
||||
final xcarchiveDirectory = artifactManager.getXcarchiveDirectory();
|
||||
|
||||
@@ -64,10 +64,11 @@ To change the version of this release, change your app's version in your pubspec
|
||||
|
||||
@override
|
||||
Future<FileSystemEntity> buildReleaseArtifacts() async {
|
||||
final base64PublicKey = await getEncodedPublicKey();
|
||||
await artifactBuilder.buildLinuxApp(
|
||||
target: target,
|
||||
args: argResults.forwardedArgs,
|
||||
base64PublicKey: argResults.encodedPublicKey,
|
||||
base64PublicKey: base64PublicKey,
|
||||
);
|
||||
|
||||
return artifactManager.linuxBundleDirectory;
|
||||
|
||||
@@ -93,12 +93,13 @@ To change the version of this release, change your app's version in your pubspec
|
||||
);
|
||||
}
|
||||
|
||||
final base64PublicKey = await getEncodedPublicKey();
|
||||
await artifactBuilder.buildMacos(
|
||||
codesign: codesign,
|
||||
flavor: flavor,
|
||||
target: target,
|
||||
args: argResults.forwardedArgs,
|
||||
base64PublicKey: argResults.encodedPublicKey,
|
||||
base64PublicKey: base64PublicKey,
|
||||
);
|
||||
|
||||
final appDirectory = artifactManager.getMacOSAppDirectory(flavor: flavor);
|
||||
|
||||
@@ -145,6 +145,10 @@ of the iOS app that is using this module. (aar and ios-framework only)''',
|
||||
CommonArguments.publicKeyArg.name,
|
||||
help: CommonArguments.publicKeyArg.description,
|
||||
)
|
||||
..addOption(
|
||||
CommonArguments.publicKeyCmd.name,
|
||||
help: CommonArguments.publicKeyCmd.description,
|
||||
)
|
||||
..addOption(
|
||||
CommonArguments.splitDebugInfoArg.name,
|
||||
help: CommonArguments.splitDebugInfoArg.description,
|
||||
@@ -367,14 +371,18 @@ of the iOS app that is using this module. (aar and ios-framework only)''',
|
||||
|
||||
/// Validates arguments that are common to all release types.
|
||||
Future<void> assertArgsAreValid(Releaser releaser) async {
|
||||
results.assertAbsentOrValidPublicKey();
|
||||
results.assertAbsentOrValidPublicKeyOrCmd();
|
||||
|
||||
final shorebirdYaml = shorebirdEnv.getShorebirdYaml();
|
||||
if (shorebirdYaml?.patchVerification != null &&
|
||||
!results.wasParsed(CommonArguments.publicKeyArg.name)) {
|
||||
final hasPublicKey =
|
||||
results.wasParsed(CommonArguments.publicKeyArg.name) ||
|
||||
results.wasParsed(CommonArguments.publicKeyCmd.name);
|
||||
if (shorebirdYaml?.patchVerification != null && !hasPublicKey) {
|
||||
logger.warn(
|
||||
'patch_verification is set in shorebird.yaml but '
|
||||
'--${CommonArguments.publicKeyArg.name} was not provided.\n'
|
||||
'no public key was provided '
|
||||
'(--${CommonArguments.publicKeyArg.name} '
|
||||
'or --${CommonArguments.publicKeyCmd.name}).\n'
|
||||
'patch_verification configuration will have no effect.',
|
||||
);
|
||||
}
|
||||
@@ -554,10 +562,13 @@ ${summary.join('\n')}
|
||||
required Release release,
|
||||
required Releaser releaser,
|
||||
}) async {
|
||||
final hasPublicKey =
|
||||
results.wasParsed(CommonArguments.publicKeyArg.name) ||
|
||||
results.wasParsed(CommonArguments.publicKeyCmd.name);
|
||||
final baseMetadata = UpdateReleaseMetadata(
|
||||
releasePlatform: releaser.releaseType.releasePlatform,
|
||||
flutterVersionOverride: flutterVersionArg,
|
||||
includesPublicKey: results.wasParsed(CommonArguments.publicKeyArg.name),
|
||||
includesPublicKey: hasPublicKey,
|
||||
environment: BuildEnvironmentMetadata(
|
||||
flutterRevision: shorebirdEnv.flutterRevision,
|
||||
operatingSystem: platform.operatingSystem,
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:pub_semver/pub_semver.dart';
|
||||
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
||||
import 'package:shorebird_cli/src/metadata/metadata.dart';
|
||||
import 'package:shorebird_cli/src/release_type.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_env.dart';
|
||||
@@ -81,4 +82,9 @@ abstract class Releaser {
|
||||
Future<String> getReleaseVersion({
|
||||
required FileSystemEntity releaseArtifactRoot,
|
||||
});
|
||||
|
||||
/// Gets the base64-encoded public key from either file or command.
|
||||
///
|
||||
/// Returns null if no public key is configured.
|
||||
Future<String?> getEncodedPublicKey() => argResults.getEncodedPublicKey();
|
||||
}
|
||||
|
||||
@@ -66,11 +66,12 @@ To change the version of this release, change your app's version in your pubspec
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FileSystemEntity> buildReleaseArtifacts() {
|
||||
Future<FileSystemEntity> buildReleaseArtifacts() async {
|
||||
final base64PublicKey = await getEncodedPublicKey();
|
||||
return artifactBuilder.buildWindowsApp(
|
||||
target: target,
|
||||
args: argResults.forwardedArgs,
|
||||
base64PublicKey: argResults.encodedPublicKey,
|
||||
base64PublicKey: base64PublicKey,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,25 @@ The path for a private key .pem file that will be used to sign the patch artifac
|
||||
''',
|
||||
);
|
||||
|
||||
/// An argument that allows the user to specify a command that outputs a
|
||||
/// PEM-encoded public key to stdout.
|
||||
static const publicKeyCmd = ArgumentDescriber(
|
||||
name: 'public-key-cmd',
|
||||
description: '''
|
||||
Command that outputs a PEM-encoded public key to stdout for patch signature validation.
|
||||
''',
|
||||
);
|
||||
|
||||
/// An argument that allows the user to specify a command that signs data.
|
||||
/// The command reads data from stdin and outputs a base64 signature to
|
||||
/// stdout.
|
||||
static const signCmd = ArgumentDescriber(
|
||||
name: 'sign-cmd',
|
||||
description: '''
|
||||
Command that reads data from stdin and outputs a base64 signature to stdout.
|
||||
''',
|
||||
);
|
||||
|
||||
/// An argument that allows the user to specify a release version. You will
|
||||
/// most likely want to provide a custom description for this argument that
|
||||
/// more thoroughly explains what the release version is used for.
|
||||
|
||||
@@ -73,6 +73,25 @@ extension CodeSign on ArgResults {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the public key PEM string from the configured source.
|
||||
///
|
||||
/// Returns null if no public key is configured. Throws
|
||||
/// [ProcessException] or [FormatException] if a command-based key
|
||||
/// fails.
|
||||
Future<String?> resolvePublicKeyPem() async {
|
||||
final publicKeyFile = file(CommonArguments.publicKeyArg.name);
|
||||
if (publicKeyFile != null) {
|
||||
return publicKeyFile.readAsStringSync();
|
||||
}
|
||||
|
||||
final publicKeyCmd = this[CommonArguments.publicKeyCmd.name] as String?;
|
||||
if (publicKeyCmd != null && wasParsed(CommonArguments.publicKeyCmd.name)) {
|
||||
return codeSigner.runPublicKeyCmd(publicKeyCmd);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Read the public key file and encode it to base64 if any.
|
||||
String? get encodedPublicKey {
|
||||
final publicKeyFile = file(CommonArguments.publicKeyArg.name);
|
||||
@@ -81,6 +100,121 @@ extension CodeSign on ArgResults {
|
||||
? codeSigner.base64PublicKey(publicKeyFile)
|
||||
: null;
|
||||
}
|
||||
|
||||
/// Validates key arguments for patch commands.
|
||||
///
|
||||
/// Valid configurations:
|
||||
/// - No signing (nothing provided)
|
||||
/// - File-based: --public-key-path + --private-key-path
|
||||
/// - Command-based: --public-key-cmd + --sign-cmd
|
||||
/// - Mixed: --public-key-path + --sign-cmd
|
||||
///
|
||||
/// Invalid configurations:
|
||||
/// - Both --public-key-path and --public-key-cmd (ambiguous public key)
|
||||
/// - Both --private-key-path and --sign-cmd (ambiguous signing method)
|
||||
/// - --sign-cmd without a public key source
|
||||
/// - --private-key-path without --public-key-path
|
||||
void assertAbsentOrValidKeyPairOrCommands() {
|
||||
final hasPublicKeyFile = wasParsed(CommonArguments.publicKeyArg.name);
|
||||
final hasPrivateKeyFile = wasParsed(CommonArguments.privateKeyArg.name);
|
||||
final hasPublicKeyCmd = wasParsed(CommonArguments.publicKeyCmd.name);
|
||||
final hasSignCmd = wasParsed(CommonArguments.signCmd.name);
|
||||
|
||||
// Can't have two public key sources
|
||||
if (hasPublicKeyFile && hasPublicKeyCmd) {
|
||||
logger.err(
|
||||
'Cannot specify both --${CommonArguments.publicKeyArg.name} and '
|
||||
'--${CommonArguments.publicKeyCmd.name}.',
|
||||
);
|
||||
throw ProcessExit(ExitCode.usage.code);
|
||||
}
|
||||
|
||||
// Can't have two signing methods
|
||||
if (hasPrivateKeyFile && hasSignCmd) {
|
||||
logger.err(
|
||||
'Cannot specify both --${CommonArguments.privateKeyArg.name} and '
|
||||
'--${CommonArguments.signCmd.name}.',
|
||||
);
|
||||
throw ProcessExit(ExitCode.usage.code);
|
||||
}
|
||||
|
||||
// File-based signing requires both file args
|
||||
if (hasPrivateKeyFile || (hasPublicKeyFile && !hasSignCmd)) {
|
||||
assertAbsentOrValidKeyPair();
|
||||
}
|
||||
|
||||
// --sign-cmd requires a public key source
|
||||
if (hasSignCmd && !hasPublicKeyFile && !hasPublicKeyCmd) {
|
||||
logger.err(
|
||||
'--${CommonArguments.signCmd.name} requires a public key '
|
||||
'(--${CommonArguments.publicKeyArg.name} or '
|
||||
'--${CommonArguments.publicKeyCmd.name}).',
|
||||
);
|
||||
throw ProcessExit(ExitCode.usage.code);
|
||||
}
|
||||
|
||||
// Validate the public key file exists if provided
|
||||
if (hasPublicKeyFile && hasSignCmd) {
|
||||
assertAbsentOrValidPublicKey();
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates public key arguments for release commands.
|
||||
///
|
||||
/// Valid configurations:
|
||||
/// - No public key (no signing)
|
||||
/// - --public-key-path with valid file
|
||||
/// - --public-key-cmd
|
||||
///
|
||||
/// Invalid: mixing --public-key-path and --public-key-cmd
|
||||
void assertAbsentOrValidPublicKeyOrCmd() {
|
||||
final hasFilePath = wasParsed(CommonArguments.publicKeyArg.name);
|
||||
final hasCmd = wasParsed(CommonArguments.publicKeyCmd.name);
|
||||
|
||||
if (hasFilePath && hasCmd) {
|
||||
logger.err(
|
||||
'Cannot specify both --${CommonArguments.publicKeyArg.name} and '
|
||||
'--${CommonArguments.publicKeyCmd.name}.',
|
||||
);
|
||||
throw ProcessExit(ExitCode.usage.code);
|
||||
}
|
||||
|
||||
if (hasFilePath) {
|
||||
assertAbsentOrValidPublicKey();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get base64-encoded public key from either file or command.
|
||||
///
|
||||
/// Returns null if no public key is configured.
|
||||
Future<String?> getEncodedPublicKey() async {
|
||||
try {
|
||||
final pem = await resolvePublicKeyPem();
|
||||
if (pem == null) return null;
|
||||
return codeSigner.base64PublicKeyFromPem(pem);
|
||||
} on ProcessException catch (e) {
|
||||
logger.err(
|
||||
'Failed to run '
|
||||
'--${CommonArguments.publicKeyCmd.name}: ${e.message}',
|
||||
);
|
||||
throw ProcessExit(ExitCode.software.code);
|
||||
} on FormatException catch (e) {
|
||||
logger.err(
|
||||
'--${CommonArguments.publicKeyCmd.name} produced invalid output: '
|
||||
'${e.message}',
|
||||
);
|
||||
throw ProcessExit(ExitCode.software.code);
|
||||
// Malformed PEM content causes ASN1 parsing errors (RangeError, etc.)
|
||||
// ignore: avoid_catching_errors
|
||||
} on Error catch (e) {
|
||||
// ASN1 parsing errors for malformed PEM content
|
||||
logger.err(
|
||||
'--${CommonArguments.publicKeyCmd.name} output is not a valid '
|
||||
'public key: $e',
|
||||
);
|
||||
throw ProcessExit(ExitCode.software.code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension on [ArgResults] to provide file related extensions.
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
// cspell:words pubin dgst outform
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:scoped_deps/scoped_deps.dart';
|
||||
import 'package:shorebird_cli/src/code_signer.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_process.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class MockShorebirdProcess extends Mock implements ShorebirdProcess {}
|
||||
|
||||
class MockShorebirdProcessResult extends Mock
|
||||
implements ShorebirdProcessResult {}
|
||||
|
||||
class MockProcess extends Mock implements Process {}
|
||||
|
||||
class MockIOSink extends Mock implements IOSink {}
|
||||
|
||||
void main() {
|
||||
group(
|
||||
CodeSigner,
|
||||
@@ -119,9 +132,250 @@ void main() {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('base64PublicKeyFromPem', () {
|
||||
test('output matches base64PublicKey from file', () {
|
||||
final publicKeyPem = publicKeyFile.readAsStringSync();
|
||||
expect(
|
||||
codeSigner.base64PublicKeyFromPem(publicKeyPem),
|
||||
equals(codeSigner.base64PublicKey(publicKeyFile)),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('verify', () {
|
||||
const message =
|
||||
'6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b';
|
||||
|
||||
test('returns true for valid signature', () {
|
||||
final signature = codeSigner.sign(
|
||||
message: message,
|
||||
privateKeyPemFile: privateKeyFile,
|
||||
);
|
||||
final publicKeyPem = publicKeyFile.readAsStringSync();
|
||||
|
||||
expect(
|
||||
codeSigner.verify(
|
||||
message: message,
|
||||
signature: signature,
|
||||
publicKeyPem: publicKeyPem,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('returns false for invalid signature', () {
|
||||
final publicKeyPem = publicKeyFile.readAsStringSync();
|
||||
|
||||
expect(
|
||||
codeSigner.verify(
|
||||
message: message,
|
||||
signature: 'invalid-signature',
|
||||
publicKeyPem: publicKeyPem,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('returns false for wrong message', () {
|
||||
final signature = codeSigner.sign(
|
||||
message: message,
|
||||
privateKeyPemFile: privateKeyFile,
|
||||
);
|
||||
final publicKeyPem = publicKeyFile.readAsStringSync();
|
||||
|
||||
expect(
|
||||
codeSigner.verify(
|
||||
message: 'wrong-message',
|
||||
signature: signature,
|
||||
publicKeyPem: publicKeyPem,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
onPlatform: {
|
||||
'windows': const Skip('Does not have openssl installed by default'),
|
||||
},
|
||||
);
|
||||
|
||||
group('CodeSigner command-based signing', () {
|
||||
late ShorebirdProcess shorebirdProcess;
|
||||
late CodeSigner codeSigner;
|
||||
|
||||
setUp(() {
|
||||
shorebirdProcess = MockShorebirdProcess();
|
||||
codeSigner = CodeSigner();
|
||||
});
|
||||
|
||||
group('runPublicKeyCmd', () {
|
||||
test('returns trimmed stdout on success', () async {
|
||||
final result = MockShorebirdProcessResult();
|
||||
when(() => result.exitCode).thenReturn(0);
|
||||
when(() => result.stdout).thenReturn('''
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
|
||||
-----END PUBLIC KEY-----
|
||||
''');
|
||||
when(
|
||||
() => shorebirdProcess.run(any(), any()),
|
||||
).thenAnswer((_) async => result);
|
||||
|
||||
await runScoped(
|
||||
() async {
|
||||
final output = await codeSigner.runPublicKeyCmd('cat key.pem');
|
||||
expect(output, contains('-----BEGIN PUBLIC KEY-----'));
|
||||
expect(output, contains('-----END PUBLIC KEY-----'));
|
||||
},
|
||||
values: {processRef.overrideWith(() => shorebirdProcess)},
|
||||
);
|
||||
});
|
||||
|
||||
test('throws ProcessException on non-zero exit code', () async {
|
||||
final result = MockShorebirdProcessResult();
|
||||
when(() => result.exitCode).thenReturn(1);
|
||||
when(() => result.stderr).thenReturn('command not found');
|
||||
when(
|
||||
() => shorebirdProcess.run(any(), any()),
|
||||
).thenAnswer((_) async => result);
|
||||
|
||||
await runScoped(
|
||||
() async {
|
||||
await expectLater(
|
||||
() => codeSigner.runPublicKeyCmd('invalid-command'),
|
||||
throwsA(isA<ProcessException>()),
|
||||
);
|
||||
},
|
||||
values: {processRef.overrideWith(() => shorebirdProcess)},
|
||||
);
|
||||
});
|
||||
|
||||
test('throws FormatException when output is empty', () async {
|
||||
final result = MockShorebirdProcessResult();
|
||||
when(() => result.exitCode).thenReturn(0);
|
||||
when(() => result.stdout).thenReturn('');
|
||||
when(
|
||||
() => shorebirdProcess.run(any(), any()),
|
||||
).thenAnswer((_) async => result);
|
||||
|
||||
await runScoped(
|
||||
() async {
|
||||
await expectLater(
|
||||
() => codeSigner.runPublicKeyCmd('empty-cmd'),
|
||||
throwsA(
|
||||
isA<FormatException>().having(
|
||||
(e) => e.message,
|
||||
'message',
|
||||
contains('produced no output'),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
values: {processRef.overrideWith(() => shorebirdProcess)},
|
||||
);
|
||||
});
|
||||
|
||||
test('throws FormatException when output is not a PEM key', () async {
|
||||
final result = MockShorebirdProcessResult();
|
||||
when(() => result.exitCode).thenReturn(0);
|
||||
when(() => result.stdout).thenReturn('not a pem key');
|
||||
when(
|
||||
() => shorebirdProcess.run(any(), any()),
|
||||
).thenAnswer((_) async => result);
|
||||
|
||||
await runScoped(
|
||||
() async {
|
||||
await expectLater(
|
||||
() => codeSigner.runPublicKeyCmd('echo "not a pem key"'),
|
||||
throwsA(isA<FormatException>()),
|
||||
);
|
||||
},
|
||||
values: {processRef.overrideWith(() => shorebirdProcess)},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('signWithCmd', () {
|
||||
late MockProcess proc;
|
||||
late MockIOSink stdin;
|
||||
|
||||
setUp(() {
|
||||
proc = MockProcess();
|
||||
stdin = MockIOSink();
|
||||
when(() => proc.stdin).thenReturn(stdin);
|
||||
when(() => stdin.close()).thenAnswer((_) async {});
|
||||
});
|
||||
|
||||
test('returns trimmed stdout on success', () async {
|
||||
when(() => proc.stdout).thenAnswer(
|
||||
(_) => Stream.value(utf8.encode('base64signature\n')),
|
||||
);
|
||||
when(() => proc.stderr).thenAnswer((_) => const Stream.empty());
|
||||
when(() => proc.exitCode).thenAnswer((_) async => 0);
|
||||
when(
|
||||
() => shorebirdProcess.start(any(), any()),
|
||||
).thenAnswer((_) async => proc);
|
||||
|
||||
await runScoped(
|
||||
() async {
|
||||
final signature = await codeSigner.signWithCmd(
|
||||
data: 'hash-to-sign',
|
||||
command: 'sign-script.sh',
|
||||
);
|
||||
expect(signature, equals('base64signature'));
|
||||
},
|
||||
values: {processRef.overrideWith(() => shorebirdProcess)},
|
||||
);
|
||||
});
|
||||
|
||||
test('throws ProcessException on non-zero exit code', () async {
|
||||
when(() => proc.stdout).thenAnswer((_) => const Stream.empty());
|
||||
when(() => proc.stderr).thenAnswer(
|
||||
(_) => Stream.value(utf8.encode('error')),
|
||||
);
|
||||
when(() => proc.exitCode).thenAnswer((_) async => 1);
|
||||
when(
|
||||
() => shorebirdProcess.start(any(), any()),
|
||||
).thenAnswer((_) async => proc);
|
||||
|
||||
await runScoped(
|
||||
() async {
|
||||
await expectLater(
|
||||
() => codeSigner.signWithCmd(
|
||||
data: 'hash-to-sign',
|
||||
command: 'failing-script.sh',
|
||||
),
|
||||
throwsA(isA<ProcessException>()),
|
||||
);
|
||||
},
|
||||
values: {processRef.overrideWith(() => shorebirdProcess)},
|
||||
);
|
||||
});
|
||||
|
||||
test('throws FormatException when output is empty', () async {
|
||||
when(() => proc.stdout).thenAnswer(
|
||||
(_) => Stream.value(utf8.encode(' \n ')),
|
||||
);
|
||||
when(() => proc.stderr).thenAnswer((_) => const Stream.empty());
|
||||
when(() => proc.exitCode).thenAnswer((_) async => 0);
|
||||
when(
|
||||
() => shorebirdProcess.start(any(), any()),
|
||||
).thenAnswer((_) async => proc);
|
||||
|
||||
await runScoped(
|
||||
() async {
|
||||
await expectLater(
|
||||
() => codeSigner.signWithCmd(
|
||||
data: 'hash-to-sign',
|
||||
command: 'empty-output-script.sh',
|
||||
),
|
||||
throwsA(isA<FormatException>()),
|
||||
);
|
||||
},
|
||||
values: {processRef.overrideWith(() => shorebirdProcess)},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -551,16 +551,18 @@ void main() {
|
||||
|
||||
group('when a private key is provided', () {
|
||||
setUp(() {
|
||||
final privateKey = File(
|
||||
p.join(
|
||||
Directory.systemTemp.createTempSync().path,
|
||||
'test-private.pem',
|
||||
),
|
||||
)..createSync();
|
||||
final tempDir = Directory.systemTemp.createTempSync();
|
||||
final privateKey = File(p.join(tempDir.path, 'test-private.pem'))
|
||||
..createSync();
|
||||
final publicKey = File(p.join(tempDir.path, 'test-public.pem'))
|
||||
..writeAsStringSync('public-key-pem');
|
||||
|
||||
when(
|
||||
() => argResults[CommonArguments.privateKeyArg.name],
|
||||
).thenReturn(privateKey.path);
|
||||
when(
|
||||
() => argResults[CommonArguments.publicKeyArg.name],
|
||||
).thenReturn(publicKey.path);
|
||||
|
||||
when(
|
||||
() => codeSigner.sign(
|
||||
@@ -571,6 +573,13 @@ void main() {
|
||||
final message = invocation.namedArguments[#message] as String;
|
||||
return '$message-signature';
|
||||
});
|
||||
when(
|
||||
() => codeSigner.verify(
|
||||
message: any(named: 'message'),
|
||||
signature: any(named: 'signature'),
|
||||
publicKeyPem: any(named: 'publicKeyPem'),
|
||||
),
|
||||
).thenReturn(true);
|
||||
});
|
||||
|
||||
test(
|
||||
|
||||
@@ -594,16 +594,18 @@ Looked in:
|
||||
|
||||
group('when a private key is provided', () {
|
||||
setUp(() {
|
||||
final privateKey = File(
|
||||
p.join(
|
||||
Directory.systemTemp.createTempSync().path,
|
||||
'test-private.pem',
|
||||
),
|
||||
)..createSync();
|
||||
final tempDir = Directory.systemTemp.createTempSync();
|
||||
final privateKey = File(p.join(tempDir.path, 'test-private.pem'))
|
||||
..createSync();
|
||||
final publicKey = File(p.join(tempDir.path, 'test-public.pem'))
|
||||
..writeAsStringSync('public-key-pem');
|
||||
|
||||
when(
|
||||
() => argResults[CommonArguments.privateKeyArg.name],
|
||||
).thenReturn(privateKey.path);
|
||||
when(
|
||||
() => argResults[CommonArguments.publicKeyArg.name],
|
||||
).thenReturn(publicKey.path);
|
||||
|
||||
when(
|
||||
() => codeSigner.sign(
|
||||
@@ -614,6 +616,13 @@ Looked in:
|
||||
final message = invocation.namedArguments[#message] as String;
|
||||
return '$message-signature';
|
||||
});
|
||||
when(
|
||||
() => codeSigner.verify(
|
||||
message: any(named: 'message'),
|
||||
signature: any(named: 'signature'),
|
||||
publicKeyPem: any(named: 'publicKeyPem'),
|
||||
),
|
||||
).thenReturn(true);
|
||||
});
|
||||
|
||||
test(
|
||||
|
||||
@@ -872,16 +872,19 @@ void main() {
|
||||
|
||||
group('when code signing the patch', () {
|
||||
setUp(() {
|
||||
final tempDir = Directory.systemTemp.createTempSync();
|
||||
final privateKey = File(
|
||||
p.join(
|
||||
Directory.systemTemp.createTempSync().path,
|
||||
'test-private.pem',
|
||||
),
|
||||
p.join(tempDir.path, 'test-private.pem'),
|
||||
)..createSync();
|
||||
final publicKey = File(p.join(tempDir.path, 'test-public.pem'))
|
||||
..writeAsStringSync('public-key-pem');
|
||||
|
||||
when(
|
||||
() => argResults[CommonArguments.privateKeyArg.name],
|
||||
).thenReturn(privateKey.path);
|
||||
when(
|
||||
() => argResults[CommonArguments.publicKeyArg.name],
|
||||
).thenReturn(publicKey.path);
|
||||
|
||||
when(
|
||||
() => codeSigner.sign(
|
||||
@@ -892,6 +895,13 @@ void main() {
|
||||
final message = invocation.namedArguments[#message] as String;
|
||||
return '$message-signature';
|
||||
});
|
||||
when(
|
||||
() => codeSigner.verify(
|
||||
message: any(named: 'message'),
|
||||
signature: any(named: 'signature'),
|
||||
publicKeyPem: any(named: 'publicKeyPem'),
|
||||
),
|
||||
).thenReturn(true);
|
||||
});
|
||||
|
||||
test(
|
||||
|
||||
@@ -1169,16 +1169,19 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}'''),
|
||||
|
||||
group('when code signing the patch', () {
|
||||
setUp(() {
|
||||
final tempDir = Directory.systemTemp.createTempSync();
|
||||
final privateKey = File(
|
||||
p.join(
|
||||
Directory.systemTemp.createTempSync().path,
|
||||
'test-private.pem',
|
||||
),
|
||||
p.join(tempDir.path, 'test-private.pem'),
|
||||
)..createSync();
|
||||
final publicKey = File(p.join(tempDir.path, 'test-public.pem'))
|
||||
..writeAsStringSync('public-key-pem');
|
||||
|
||||
when(
|
||||
() => argResults[CommonArguments.privateKeyArg.name],
|
||||
).thenReturn(privateKey.path);
|
||||
when(
|
||||
() => argResults[CommonArguments.publicKeyArg.name],
|
||||
).thenReturn(publicKey.path);
|
||||
|
||||
when(
|
||||
() => codeSigner.sign(
|
||||
@@ -1189,6 +1192,13 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}'''),
|
||||
final message = invocation.namedArguments[#message] as String;
|
||||
return '$message-signature';
|
||||
});
|
||||
when(
|
||||
() => codeSigner.verify(
|
||||
message: any(named: 'message'),
|
||||
signature: any(named: 'signature'),
|
||||
publicKeyPem: any(named: 'publicKeyPem'),
|
||||
),
|
||||
).thenReturn(true);
|
||||
});
|
||||
|
||||
test(
|
||||
|
||||
@@ -366,6 +366,12 @@ void main() {
|
||||
|
||||
group('when signing keys are provided', () {
|
||||
setUp(() {
|
||||
final publicKeyFile = File(
|
||||
p.join(
|
||||
Directory.systemTemp.createTempSync().path,
|
||||
'public-key.pem',
|
||||
),
|
||||
)..createSync();
|
||||
when(
|
||||
() => artifactManager.createDiff(
|
||||
releaseArtifactPath: any(named: 'releaseArtifactPath'),
|
||||
@@ -374,7 +380,7 @@ void main() {
|
||||
).thenAnswer((_) async => diffFile.path);
|
||||
when(
|
||||
() => argResults[CommonArguments.publicKeyArg.name],
|
||||
).thenReturn('public-key.pem');
|
||||
).thenReturn(publicKeyFile.path);
|
||||
when(
|
||||
() => argResults[CommonArguments.privateKeyArg.name],
|
||||
).thenReturn('private-key.pem');
|
||||
@@ -384,6 +390,13 @@ void main() {
|
||||
privateKeyPemFile: any(named: 'privateKeyPemFile'),
|
||||
),
|
||||
).thenReturn('signature');
|
||||
when(
|
||||
() => codeSigner.verify(
|
||||
message: any(named: 'message'),
|
||||
signature: any(named: 'signature'),
|
||||
publicKeyPem: any(named: 'publicKeyPem'),
|
||||
),
|
||||
).thenReturn(true);
|
||||
});
|
||||
|
||||
test('signs patch', () async {
|
||||
|
||||
@@ -952,6 +952,13 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}'''),
|
||||
when(
|
||||
() => argResults[CommonArguments.privateKeyArg.name],
|
||||
).thenReturn(createTempFile('private.pem').path);
|
||||
when(
|
||||
() => argResults[CommonArguments.publicKeyArg.name],
|
||||
).thenReturn(
|
||||
(createTempFile(
|
||||
'public.pem',
|
||||
)..writeAsStringSync('public-key-pem')).path,
|
||||
);
|
||||
|
||||
when(
|
||||
() => codeSigner.sign(
|
||||
@@ -959,6 +966,13 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}'''),
|
||||
privateKeyPemFile: any(named: 'privateKeyPemFile'),
|
||||
),
|
||||
).thenReturn('my-signature');
|
||||
when(
|
||||
() => codeSigner.verify(
|
||||
message: any(named: 'message'),
|
||||
signature: any(named: 'signature'),
|
||||
publicKeyPem: any(named: 'publicKeyPem'),
|
||||
),
|
||||
).thenReturn(true);
|
||||
});
|
||||
|
||||
test('returns artifact bundles with non-null hash signature', () async {
|
||||
|
||||
@@ -194,6 +194,12 @@ void main() {
|
||||
when(
|
||||
() => argResults.wasParsed(CommonArguments.publicKeyArg.name),
|
||||
).thenReturn(false);
|
||||
when(
|
||||
() => argResults.wasParsed(CommonArguments.publicKeyCmd.name),
|
||||
).thenReturn(false);
|
||||
when(
|
||||
() => argResults.wasParsed(CommonArguments.signCmd.name),
|
||||
).thenReturn(false);
|
||||
|
||||
when(aotTools.isLinkDebugInfoSupported).thenAnswer((_) async => true);
|
||||
|
||||
|
||||
@@ -2,24 +2,31 @@ import 'dart:io';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:scoped_deps/scoped_deps.dart';
|
||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||
import 'package:shorebird_cli/src/code_signer.dart';
|
||||
import 'package:shorebird_cli/src/commands/commands.dart';
|
||||
import 'package:shorebird_cli/src/common_arguments.dart';
|
||||
import 'package:shorebird_cli/src/deployment_track.dart';
|
||||
import 'package:shorebird_cli/src/logging/logging.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_client/shorebird_code_push_client.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../../mocks.dart';
|
||||
|
||||
class FakeFile extends Fake implements File {}
|
||||
|
||||
void main() {
|
||||
group(Patcher, () {
|
||||
setUpAll(() {
|
||||
registerFallbackValue(ReleasePlatform.android);
|
||||
registerFallbackValue(DeploymentTrack.stable);
|
||||
registerFallbackValue(FakeFile());
|
||||
});
|
||||
|
||||
group('linkPercentage', () {
|
||||
@@ -236,6 +243,358 @@ void main() {
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('signHash', () {
|
||||
final cryptoFixturesBasePath = p.join('test', 'fixtures', 'crypto');
|
||||
final privateKeyFile = File(
|
||||
p.join(cryptoFixturesBasePath, 'private.pem'),
|
||||
);
|
||||
|
||||
late ArgParser argParser;
|
||||
late ArgResults argResults;
|
||||
late CodeSigner codeSigner;
|
||||
late ShorebirdLogger logger;
|
||||
late File publicKeyTempFile;
|
||||
|
||||
setUp(() {
|
||||
argParser = ArgParser()
|
||||
..addOption(CommonArguments.publicKeyArg.name)
|
||||
..addOption(CommonArguments.privateKeyArg.name)
|
||||
..addOption(CommonArguments.publicKeyCmd.name)
|
||||
..addOption(CommonArguments.signCmd.name);
|
||||
codeSigner = MockCodeSigner();
|
||||
logger = MockShorebirdLogger();
|
||||
publicKeyTempFile = File(
|
||||
p.join(
|
||||
Directory.systemTemp.createTempSync().path,
|
||||
'public.pem',
|
||||
),
|
||||
)..writeAsStringSync('fake-public-key-pem');
|
||||
});
|
||||
|
||||
test('returns null when no signing is configured', () async {
|
||||
argResults = argParser.parse([]);
|
||||
|
||||
final patcher = _TestPatcher(
|
||||
argParser: argParser,
|
||||
argResults: argResults,
|
||||
flavor: null,
|
||||
target: null,
|
||||
);
|
||||
|
||||
await runScoped(
|
||||
() async {
|
||||
final result = await patcher.signHash('test-hash');
|
||||
expect(result, isNull);
|
||||
},
|
||||
values: {codeSignerRef.overrideWith(() => codeSigner)},
|
||||
);
|
||||
});
|
||||
|
||||
test('returns signature from file-based signing', () async {
|
||||
argResults = argParser.parse([
|
||||
'--${CommonArguments.publicKeyArg.name}=${publicKeyTempFile.path}',
|
||||
'--${CommonArguments.privateKeyArg.name}=${privateKeyFile.path}',
|
||||
]);
|
||||
|
||||
when(
|
||||
() => codeSigner.sign(
|
||||
message: any(named: 'message'),
|
||||
privateKeyPemFile: any(named: 'privateKeyPemFile'),
|
||||
),
|
||||
).thenReturn('file-signature');
|
||||
when(
|
||||
() => codeSigner.verify(
|
||||
message: any(named: 'message'),
|
||||
signature: any(named: 'signature'),
|
||||
publicKeyPem: any(named: 'publicKeyPem'),
|
||||
),
|
||||
).thenReturn(true);
|
||||
|
||||
final patcher = _TestPatcher(
|
||||
argParser: argParser,
|
||||
argResults: argResults,
|
||||
flavor: null,
|
||||
target: null,
|
||||
);
|
||||
|
||||
await runScoped(
|
||||
() async {
|
||||
final result = await patcher.signHash('test-hash');
|
||||
expect(result, equals('file-signature'));
|
||||
},
|
||||
values: {codeSignerRef.overrideWith(() => codeSigner)},
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'throws ProcessExit when signer present but no public key',
|
||||
() async {
|
||||
argResults = argParser.parse([
|
||||
'--${CommonArguments.privateKeyArg.name}=${privateKeyFile.path}',
|
||||
]);
|
||||
|
||||
when(
|
||||
() => codeSigner.sign(
|
||||
message: any(named: 'message'),
|
||||
privateKeyPemFile: any(named: 'privateKeyPemFile'),
|
||||
),
|
||||
).thenReturn('file-signature');
|
||||
|
||||
final patcher = _TestPatcher(
|
||||
argParser: argParser,
|
||||
argResults: argResults,
|
||||
flavor: null,
|
||||
target: null,
|
||||
);
|
||||
|
||||
await runScoped(
|
||||
() async {
|
||||
await expectLater(
|
||||
() => patcher.signHash('test-hash'),
|
||||
throwsA(isA<ProcessExit>()),
|
||||
);
|
||||
verify(
|
||||
() => logger.err(
|
||||
any(that: contains('public key is required')),
|
||||
),
|
||||
).called(1);
|
||||
},
|
||||
values: {
|
||||
codeSignerRef.overrideWith(() => codeSigner),
|
||||
loggerRef.overrideWith(() => logger),
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('returns signature from command-based signing when valid', () async {
|
||||
argResults = argParser.parse([
|
||||
'--${CommonArguments.publicKeyCmd.name}=get-key-cmd',
|
||||
'--${CommonArguments.signCmd.name}=sign-cmd',
|
||||
]);
|
||||
|
||||
when(
|
||||
() => codeSigner.signWithCmd(
|
||||
data: any(named: 'data'),
|
||||
command: any(named: 'command'),
|
||||
),
|
||||
).thenAnswer((_) async => 'cmd-signature');
|
||||
when(
|
||||
() => codeSigner.runPublicKeyCmd(any()),
|
||||
).thenAnswer((_) async => 'pem-public-key');
|
||||
when(
|
||||
() => codeSigner.verify(
|
||||
message: any(named: 'message'),
|
||||
signature: any(named: 'signature'),
|
||||
publicKeyPem: any(named: 'publicKeyPem'),
|
||||
),
|
||||
).thenReturn(true);
|
||||
|
||||
final patcher = _TestPatcher(
|
||||
argParser: argParser,
|
||||
argResults: argResults,
|
||||
flavor: null,
|
||||
target: null,
|
||||
);
|
||||
|
||||
await runScoped(
|
||||
() async {
|
||||
final result = await patcher.signHash('test-hash');
|
||||
expect(result, equals('cmd-signature'));
|
||||
verify(
|
||||
() => codeSigner.signWithCmd(
|
||||
data: 'test-hash',
|
||||
command: 'sign-cmd',
|
||||
),
|
||||
).called(1);
|
||||
verify(() => codeSigner.runPublicKeyCmd('get-key-cmd')).called(1);
|
||||
verify(
|
||||
() => codeSigner.verify(
|
||||
message: 'test-hash',
|
||||
signature: 'cmd-signature',
|
||||
publicKeyPem: 'pem-public-key',
|
||||
),
|
||||
).called(1);
|
||||
},
|
||||
values: {codeSignerRef.overrideWith(() => codeSigner)},
|
||||
);
|
||||
});
|
||||
|
||||
test('throws ProcessExit when sign-cmd fails', () async {
|
||||
argResults = argParser.parse([
|
||||
'--${CommonArguments.publicKeyCmd.name}=get-key-cmd',
|
||||
'--${CommonArguments.signCmd.name}=bad-cmd',
|
||||
]);
|
||||
|
||||
when(
|
||||
() => codeSigner.signWithCmd(
|
||||
data: any(named: 'data'),
|
||||
command: any(named: 'command'),
|
||||
),
|
||||
).thenThrow(
|
||||
const ProcessException('bad-cmd', [], 'command not found', 127),
|
||||
);
|
||||
|
||||
final patcher = _TestPatcher(
|
||||
argParser: argParser,
|
||||
argResults: argResults,
|
||||
flavor: null,
|
||||
target: null,
|
||||
);
|
||||
|
||||
await runScoped(
|
||||
() async {
|
||||
await expectLater(
|
||||
() => patcher.signHash('test-hash'),
|
||||
throwsA(isA<ProcessExit>()),
|
||||
);
|
||||
verify(
|
||||
() => logger.err(any(that: contains('--sign-cmd'))),
|
||||
).called(1);
|
||||
},
|
||||
values: {
|
||||
codeSignerRef.overrideWith(() => codeSigner),
|
||||
loggerRef.overrideWith(() => logger),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('throws ProcessExit when public-key-cmd fails', () async {
|
||||
argResults = argParser.parse([
|
||||
'--${CommonArguments.publicKeyCmd.name}=bad-key-cmd',
|
||||
'--${CommonArguments.signCmd.name}=sign-cmd',
|
||||
]);
|
||||
|
||||
when(
|
||||
() => codeSigner.signWithCmd(
|
||||
data: any(named: 'data'),
|
||||
command: any(named: 'command'),
|
||||
),
|
||||
).thenAnswer((_) async => 'signature');
|
||||
when(
|
||||
() => codeSigner.runPublicKeyCmd(any()),
|
||||
).thenThrow(
|
||||
const ProcessException(
|
||||
'bad-key-cmd',
|
||||
[],
|
||||
'command not found',
|
||||
127,
|
||||
),
|
||||
);
|
||||
|
||||
final patcher = _TestPatcher(
|
||||
argParser: argParser,
|
||||
argResults: argResults,
|
||||
flavor: null,
|
||||
target: null,
|
||||
);
|
||||
|
||||
await runScoped(
|
||||
() async {
|
||||
await expectLater(
|
||||
() => patcher.signHash('test-hash'),
|
||||
throwsA(isA<ProcessExit>()),
|
||||
);
|
||||
verify(
|
||||
() => logger.err(any(that: contains('--public-key-cmd'))),
|
||||
).called(1);
|
||||
},
|
||||
values: {
|
||||
codeSignerRef.overrideWith(() => codeSigner),
|
||||
loggerRef.overrideWith(() => logger),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('supports mixed signing (public key file + sign cmd)', () async {
|
||||
argResults = argParser.parse([
|
||||
'--${CommonArguments.publicKeyArg.name}=${publicKeyTempFile.path}',
|
||||
'--${CommonArguments.signCmd.name}=sign-cmd',
|
||||
]);
|
||||
|
||||
when(
|
||||
() => codeSigner.signWithCmd(
|
||||
data: any(named: 'data'),
|
||||
command: any(named: 'command'),
|
||||
),
|
||||
).thenAnswer((_) async => 'cmd-signature');
|
||||
when(
|
||||
() => codeSigner.verify(
|
||||
message: any(named: 'message'),
|
||||
signature: any(named: 'signature'),
|
||||
publicKeyPem: any(named: 'publicKeyPem'),
|
||||
),
|
||||
).thenReturn(true);
|
||||
|
||||
final patcher = _TestPatcher(
|
||||
argParser: argParser,
|
||||
argResults: argResults,
|
||||
flavor: null,
|
||||
target: null,
|
||||
);
|
||||
|
||||
await runScoped(
|
||||
() async {
|
||||
final result = await patcher.signHash('test-hash');
|
||||
expect(result, equals('cmd-signature'));
|
||||
verifyNever(() => codeSigner.runPublicKeyCmd(any()));
|
||||
verify(
|
||||
() => codeSigner.verify(
|
||||
message: 'test-hash',
|
||||
signature: 'cmd-signature',
|
||||
publicKeyPem: any(named: 'publicKeyPem'),
|
||||
),
|
||||
).called(1);
|
||||
},
|
||||
values: {codeSignerRef.overrideWith(() => codeSigner)},
|
||||
);
|
||||
});
|
||||
|
||||
test('throws ProcessExit when signature verification fails', () async {
|
||||
argResults = argParser.parse([
|
||||
'--${CommonArguments.publicKeyCmd.name}=get-key-cmd',
|
||||
'--${CommonArguments.signCmd.name}=sign-cmd',
|
||||
]);
|
||||
|
||||
when(
|
||||
() => codeSigner.signWithCmd(
|
||||
data: any(named: 'data'),
|
||||
command: any(named: 'command'),
|
||||
),
|
||||
).thenAnswer((_) async => 'bad-signature');
|
||||
when(
|
||||
() => codeSigner.runPublicKeyCmd(any()),
|
||||
).thenAnswer((_) async => 'pem-public-key');
|
||||
when(
|
||||
() => codeSigner.verify(
|
||||
message: any(named: 'message'),
|
||||
signature: any(named: 'signature'),
|
||||
publicKeyPem: any(named: 'publicKeyPem'),
|
||||
),
|
||||
).thenReturn(false);
|
||||
|
||||
final patcher = _TestPatcher(
|
||||
argParser: argParser,
|
||||
argResults: argResults,
|
||||
flavor: null,
|
||||
target: null,
|
||||
);
|
||||
|
||||
await runScoped(
|
||||
() async {
|
||||
await expectLater(
|
||||
() => patcher.signHash('test-hash'),
|
||||
throwsA(isA<ProcessExit>()),
|
||||
);
|
||||
},
|
||||
values: {
|
||||
codeSignerRef.overrideWith(() => codeSigner),
|
||||
loggerRef.overrideWith(() => logger),
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -467,6 +467,12 @@ void main() {
|
||||
|
||||
group('when signing keys are provided', () {
|
||||
setUp(() {
|
||||
final publicKeyFile = File(
|
||||
p.join(
|
||||
Directory.systemTemp.createTempSync().path,
|
||||
'public-key.pem',
|
||||
),
|
||||
)..createSync();
|
||||
when(
|
||||
() => artifactManager.createDiff(
|
||||
releaseArtifactPath: any(named: 'releaseArtifactPath'),
|
||||
@@ -475,7 +481,7 @@ void main() {
|
||||
).thenAnswer((_) async => diffFile.path);
|
||||
when(
|
||||
() => argResults[CommonArguments.publicKeyArg.name],
|
||||
).thenReturn('public-key.pem');
|
||||
).thenReturn(publicKeyFile.path);
|
||||
when(
|
||||
() => argResults[CommonArguments.privateKeyArg.name],
|
||||
).thenReturn('private-key.pem');
|
||||
@@ -485,6 +491,13 @@ void main() {
|
||||
privateKeyPemFile: any(named: 'privateKeyPemFile'),
|
||||
),
|
||||
).thenReturn('signature');
|
||||
when(
|
||||
() => codeSigner.verify(
|
||||
message: any(named: 'message'),
|
||||
signature: any(named: 'signature'),
|
||||
publicKeyPem: any(named: 'publicKeyPem'),
|
||||
),
|
||||
).thenReturn(true);
|
||||
});
|
||||
|
||||
test('signs patch', () async {
|
||||
|
||||
@@ -326,6 +326,9 @@ void main() {
|
||||
when(
|
||||
() => argResults[CommonArguments.publicKeyArg.name],
|
||||
).thenReturn(patchSigningPublicKeyFile.path);
|
||||
when(
|
||||
() => argResults.wasParsed(CommonArguments.publicKeyArg.name),
|
||||
).thenReturn(true);
|
||||
|
||||
when(
|
||||
() => artifactBuilder.buildAar(
|
||||
@@ -337,7 +340,7 @@ void main() {
|
||||
).thenAnswer((_) async => File(''));
|
||||
|
||||
when(
|
||||
() => codeSigner.base64PublicKey(any()),
|
||||
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||
).thenReturn(base64PublicKey);
|
||||
});
|
||||
|
||||
@@ -357,6 +360,57 @@ void main() {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('when a public-key-cmd is provided', () {
|
||||
const base64PublicKey = 'base64PublicKeyFromCmd';
|
||||
|
||||
setUp(() {
|
||||
when(
|
||||
() => argResults[CommonArguments.publicKeyCmd.name],
|
||||
).thenReturn('get-key-cmd');
|
||||
when(
|
||||
() => argResults.wasParsed(CommonArguments.publicKeyCmd.name),
|
||||
).thenReturn(true);
|
||||
|
||||
when(
|
||||
() => artifactBuilder.buildAar(
|
||||
buildNumber: any(named: 'buildNumber'),
|
||||
targetPlatforms: any(named: 'targetPlatforms'),
|
||||
args: any(named: 'args'),
|
||||
base64PublicKey: any(named: 'base64PublicKey'),
|
||||
),
|
||||
).thenAnswer((_) async => File(''));
|
||||
|
||||
when(
|
||||
() => codeSigner.runPublicKeyCmd(any()),
|
||||
).thenAnswer((_) async => 'pem-public-key');
|
||||
when(
|
||||
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||
).thenReturn(base64PublicKey);
|
||||
});
|
||||
|
||||
test(
|
||||
'runs public key cmd and forwards encoded key to buildAar',
|
||||
() async {
|
||||
await runWithOverrides(() => aarReleaser.buildReleaseArtifacts());
|
||||
|
||||
verify(
|
||||
() => codeSigner.runPublicKeyCmd('get-key-cmd'),
|
||||
).called(1);
|
||||
verify(
|
||||
() => codeSigner.base64PublicKeyFromPem('pem-public-key'),
|
||||
).called(1);
|
||||
verify(
|
||||
() => artifactBuilder.buildAar(
|
||||
buildNumber: buildNumber,
|
||||
targetPlatforms: any(named: 'targetPlatforms'),
|
||||
args: any(named: 'args'),
|
||||
base64PublicKey: base64PublicKey,
|
||||
),
|
||||
).called(1);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -417,6 +417,9 @@ To change the version of this release, change your app's version in your pubspec
|
||||
when(
|
||||
() => argResults[CommonArguments.publicKeyArg.name],
|
||||
).thenReturn(patchSigningPublicKeyFile.path);
|
||||
when(
|
||||
() => argResults.wasParsed(CommonArguments.publicKeyArg.name),
|
||||
).thenReturn(true);
|
||||
|
||||
when(
|
||||
() => artifactBuilder.buildAppBundle(
|
||||
@@ -438,7 +441,7 @@ To change the version of this release, change your app's version in your pubspec
|
||||
).thenAnswer((_) async => File(''));
|
||||
|
||||
when(
|
||||
() => codeSigner.base64PublicKey(any()),
|
||||
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||
).thenReturn(base64PublicKey);
|
||||
});
|
||||
|
||||
@@ -496,6 +499,61 @@ To change the version of this release, change your app's version in your pubspec
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when a public-key-cmd is provided', () {
|
||||
const base64PublicKey = 'base64PublicKeyFromCmd';
|
||||
|
||||
setUp(() {
|
||||
when(
|
||||
() => argResults[CommonArguments.publicKeyCmd.name],
|
||||
).thenReturn('get-key-cmd');
|
||||
when(
|
||||
() => argResults.wasParsed(CommonArguments.publicKeyCmd.name),
|
||||
).thenReturn(true);
|
||||
|
||||
when(
|
||||
() => artifactBuilder.buildAppBundle(
|
||||
flavor: any(named: 'flavor'),
|
||||
target: any(named: 'target'),
|
||||
targetPlatforms: any(named: 'targetPlatforms'),
|
||||
args: any(named: 'args'),
|
||||
base64PublicKey: any(named: 'base64PublicKey'),
|
||||
),
|
||||
).thenAnswer((_) async => aabFile);
|
||||
|
||||
when(
|
||||
() => codeSigner.runPublicKeyCmd(any()),
|
||||
).thenAnswer((_) async => 'pem-public-key');
|
||||
when(
|
||||
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||
).thenReturn(base64PublicKey);
|
||||
});
|
||||
|
||||
test(
|
||||
'runs public key cmd and forwards encoded key to buildAab',
|
||||
() async {
|
||||
await runWithOverrides(
|
||||
() => androidReleaser.buildReleaseArtifacts(),
|
||||
);
|
||||
|
||||
verify(
|
||||
() => codeSigner.runPublicKeyCmd('get-key-cmd'),
|
||||
).called(1);
|
||||
verify(
|
||||
() => codeSigner.base64PublicKeyFromPem('pem-public-key'),
|
||||
).called(1);
|
||||
verify(
|
||||
() => artifactBuilder.buildAppBundle(
|
||||
flavor: any(named: 'flavor'),
|
||||
target: any(named: 'target'),
|
||||
targetPlatforms: any(named: 'targetPlatforms'),
|
||||
args: any(named: 'args'),
|
||||
base64PublicKey: base64PublicKey,
|
||||
),
|
||||
).called(1);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('getReleaseVersion', () {
|
||||
|
||||
@@ -293,6 +293,9 @@ void main() {
|
||||
when(
|
||||
() => argResults[CommonArguments.publicKeyArg.name],
|
||||
).thenReturn(patchSigningPublicKeyFile.path);
|
||||
when(
|
||||
() => argResults.wasParsed(CommonArguments.publicKeyArg.name),
|
||||
).thenReturn(true);
|
||||
|
||||
when(
|
||||
() => artifactBuilder.buildIosFramework(
|
||||
@@ -304,7 +307,7 @@ void main() {
|
||||
AppleBuildResult(kernelFile: File('/path/to/app.dill')),
|
||||
);
|
||||
when(
|
||||
() => codeSigner.base64PublicKey(any()),
|
||||
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||
).thenReturn(base64PublicKey);
|
||||
});
|
||||
|
||||
@@ -326,6 +329,59 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
group('when a public-key-cmd is provided', () {
|
||||
const base64PublicKey = 'base64PublicKeyFromCmd';
|
||||
|
||||
setUp(() {
|
||||
when(
|
||||
() => argResults[CommonArguments.publicKeyCmd.name],
|
||||
).thenReturn('get-key-cmd');
|
||||
when(
|
||||
() => argResults.wasParsed(CommonArguments.publicKeyCmd.name),
|
||||
).thenReturn(true);
|
||||
|
||||
when(
|
||||
() => artifactBuilder.buildIosFramework(
|
||||
args: any(named: 'args'),
|
||||
base64PublicKey: any(named: 'base64PublicKey'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async =>
|
||||
AppleBuildResult(kernelFile: File('/path/to/app.dill')),
|
||||
);
|
||||
|
||||
when(
|
||||
() => codeSigner.runPublicKeyCmd(any()),
|
||||
).thenAnswer((_) async => 'pem-public-key');
|
||||
when(
|
||||
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||
).thenReturn(base64PublicKey);
|
||||
});
|
||||
|
||||
test(
|
||||
'runs public key cmd and forwards encoded key to '
|
||||
'buildIosFramework',
|
||||
() async {
|
||||
await runWithOverrides(
|
||||
() => iosFrameworkReleaser.buildReleaseArtifacts(),
|
||||
);
|
||||
|
||||
verify(
|
||||
() => codeSigner.runPublicKeyCmd('get-key-cmd'),
|
||||
).called(1);
|
||||
verify(
|
||||
() => codeSigner.base64PublicKeyFromPem('pem-public-key'),
|
||||
).called(1);
|
||||
verify(
|
||||
() => artifactBuilder.buildIosFramework(
|
||||
args: any(named: 'args'),
|
||||
base64PublicKey: base64PublicKey,
|
||||
),
|
||||
).called(1);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('when stale build/ios/shorebird directory exists', () {
|
||||
late Directory shorebirdSupplementDir;
|
||||
|
||||
|
||||
@@ -310,7 +310,7 @@ To change the version of this release, change your app's version in your pubspec
|
||||
).thenReturn(xcarchiveDirectory);
|
||||
|
||||
when(
|
||||
() => codeSigner.base64PublicKey(any()),
|
||||
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||
).thenReturn(base64PublicKey);
|
||||
|
||||
when(
|
||||
@@ -332,6 +332,9 @@ To change the version of this release, change your app's version in your pubspec
|
||||
when(
|
||||
() => argResults[CommonArguments.publicKeyArg.name],
|
||||
).thenReturn(patchSigningPublicKeyFile.path);
|
||||
when(
|
||||
() => argResults.wasParsed(CommonArguments.publicKeyArg.name),
|
||||
).thenReturn(true);
|
||||
|
||||
when(
|
||||
() => artifactBuilder.buildIpa(
|
||||
@@ -365,6 +368,60 @@ To change the version of this release, change your app's version in your pubspec
|
||||
);
|
||||
});
|
||||
|
||||
group('when a public-key-cmd is provided', () {
|
||||
setUp(() {
|
||||
when(
|
||||
() => argResults[CommonArguments.publicKeyCmd.name],
|
||||
).thenReturn('get-key-cmd');
|
||||
when(
|
||||
() => argResults.wasParsed(CommonArguments.publicKeyCmd.name),
|
||||
).thenReturn(true);
|
||||
|
||||
when(
|
||||
() => artifactBuilder.buildIpa(
|
||||
codesign: any(named: 'codesign'),
|
||||
flavor: any(named: 'flavor'),
|
||||
target: any(named: 'target'),
|
||||
args: any(named: 'args'),
|
||||
base64PublicKey: any(named: 'base64PublicKey'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async =>
|
||||
AppleBuildResult(kernelFile: File('/path/to/app.dill')),
|
||||
);
|
||||
|
||||
when(
|
||||
() => codeSigner.runPublicKeyCmd(any()),
|
||||
).thenAnswer((_) async => 'pem-public-key');
|
||||
when(
|
||||
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||
).thenReturn('base64PublicKeyFromCmd');
|
||||
});
|
||||
|
||||
test(
|
||||
'runs public key cmd and forwards encoded key to buildIpa',
|
||||
() async {
|
||||
await runWithOverrides(() => iosReleaser.buildReleaseArtifacts());
|
||||
|
||||
verify(
|
||||
() => codeSigner.runPublicKeyCmd('get-key-cmd'),
|
||||
).called(1);
|
||||
verify(
|
||||
() => codeSigner.base64PublicKeyFromPem('pem-public-key'),
|
||||
).called(1);
|
||||
verify(
|
||||
() => artifactBuilder.buildIpa(
|
||||
codesign: any(named: 'codesign'),
|
||||
flavor: any(named: 'flavor'),
|
||||
target: any(named: 'target'),
|
||||
args: any(named: 'args'),
|
||||
base64PublicKey: 'base64PublicKeyFromCmd',
|
||||
),
|
||||
).called(1);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('when not codesigning', () {
|
||||
setUp(() {
|
||||
when(() => argResults['codesign']).thenReturn(false);
|
||||
|
||||
@@ -296,14 +296,20 @@ To change the version of this release, change your app's version in your pubspec
|
||||
|
||||
group('when public key is passed as an arg', () {
|
||||
setUp(() {
|
||||
final publicKeyFile = File(
|
||||
p.join(
|
||||
Directory.systemTemp.createTempSync().path,
|
||||
'public-key.pem',
|
||||
),
|
||||
)..createSync(recursive: true);
|
||||
when(
|
||||
() => argResults.wasParsed(CommonArguments.publicKeyArg.name),
|
||||
).thenReturn(true);
|
||||
when(
|
||||
() => argResults[CommonArguments.publicKeyArg.name],
|
||||
).thenReturn('public_key');
|
||||
).thenReturn(publicKeyFile.path);
|
||||
when(
|
||||
() => codeSigner.base64PublicKey(any()),
|
||||
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||
).thenReturn('encoded_public_key');
|
||||
});
|
||||
|
||||
@@ -318,6 +324,38 @@ To change the version of this release, change your app's version in your pubspec
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('when a public-key-cmd is provided', () {
|
||||
setUp(() {
|
||||
when(
|
||||
() => argResults[CommonArguments.publicKeyCmd.name],
|
||||
).thenReturn('get-key-cmd');
|
||||
when(
|
||||
() => argResults.wasParsed(CommonArguments.publicKeyCmd.name),
|
||||
).thenReturn(true);
|
||||
|
||||
when(
|
||||
() => codeSigner.runPublicKeyCmd(any()),
|
||||
).thenAnswer((_) async => 'pem-public-key');
|
||||
when(
|
||||
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||
).thenReturn('encoded_public_key_from_cmd');
|
||||
});
|
||||
|
||||
test('passes public key to buildLinuxApp', () async {
|
||||
await runWithOverrides(releaser.buildReleaseArtifacts);
|
||||
verify(
|
||||
() => codeSigner.runPublicKeyCmd('get-key-cmd'),
|
||||
).called(1);
|
||||
verify(
|
||||
() => artifactBuilder.buildLinuxApp(
|
||||
base64PublicKey: 'encoded_public_key_from_cmd',
|
||||
target: any(named: 'target'),
|
||||
args: any(named: 'args'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('getReleaseVersion', () {
|
||||
|
||||
@@ -11,7 +11,9 @@ import 'package:scoped_deps/scoped_deps.dart';
|
||||
import 'package:shorebird_cli/src/artifact_builder/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/code_signer.dart';
|
||||
import 'package:shorebird_cli/src/commands/release/release.dart';
|
||||
import 'package:shorebird_cli/src/common_arguments.dart';
|
||||
import 'package:shorebird_cli/src/config/config.dart';
|
||||
import 'package:shorebird_cli/src/doctor.dart';
|
||||
import 'package:shorebird_cli/src/executables/xcodebuild.dart';
|
||||
@@ -35,6 +37,7 @@ void main() {
|
||||
late ArtifactBuilder artifactBuilder;
|
||||
late ArtifactManager artifactManager;
|
||||
late CodePushClientWrapper codePushClientWrapper;
|
||||
late CodeSigner codeSigner;
|
||||
late Directory projectRoot;
|
||||
late Doctor doctor;
|
||||
late FlavorValidator flavorValidator;
|
||||
@@ -54,6 +57,7 @@ void main() {
|
||||
artifactBuilderRef.overrideWith(() => artifactBuilder),
|
||||
artifactManagerRef.overrideWith(() => artifactManager),
|
||||
codePushClientWrapperRef.overrideWith(() => codePushClientWrapper),
|
||||
codeSignerRef.overrideWith(() => codeSigner),
|
||||
doctorRef.overrideWith(() => doctor),
|
||||
loggerRef.overrideWith(() => logger),
|
||||
shorebirdEnvRef.overrideWith(() => shorebirdEnv),
|
||||
@@ -69,6 +73,7 @@ void main() {
|
||||
artifactBuilder = MockArtifactBuilder();
|
||||
artifactManager = MockArtifactManager();
|
||||
codePushClientWrapper = MockCodePushClientWrapper();
|
||||
codeSigner = MockCodeSigner();
|
||||
doctor = MockDoctor();
|
||||
flavorValidator = MockFlavorValidator();
|
||||
projectRoot = Directory.systemTemp.createTempSync();
|
||||
@@ -269,6 +274,7 @@ To change the version of this release, change your app's version in your pubspec
|
||||
flavor: any(named: 'flavor'),
|
||||
target: any(named: 'target'),
|
||||
args: any(named: 'args'),
|
||||
base64PublicKey: any(named: 'base64PublicKey'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => AppleBuildResult(kernelFile: File('/path/to/app.dill')),
|
||||
@@ -356,6 +362,41 @@ To change the version of this release, change your app's version in your pubspec
|
||||
});
|
||||
});
|
||||
|
||||
group('when a public-key-cmd is provided', () {
|
||||
setUp(() {
|
||||
when(
|
||||
() => argResults[CommonArguments.publicKeyCmd.name],
|
||||
).thenReturn('get-key-cmd');
|
||||
when(
|
||||
() => argResults.wasParsed(CommonArguments.publicKeyCmd.name),
|
||||
).thenReturn(true);
|
||||
|
||||
when(
|
||||
() => codeSigner.runPublicKeyCmd(any()),
|
||||
).thenAnswer((_) async => 'pem-public-key');
|
||||
when(
|
||||
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||
).thenReturn('base64PublicKeyFromCmd');
|
||||
});
|
||||
|
||||
test('runs public key cmd and forwards encoded key', () async {
|
||||
await runWithOverrides(releaser.buildReleaseArtifacts);
|
||||
|
||||
verify(
|
||||
() => codeSigner.runPublicKeyCmd('get-key-cmd'),
|
||||
).called(1);
|
||||
verify(
|
||||
() => codeSigner.base64PublicKeyFromPem('pem-public-key'),
|
||||
).called(1);
|
||||
verify(
|
||||
() => artifactBuilder.buildMacos(
|
||||
base64PublicKey: 'base64PublicKeyFromCmd',
|
||||
args: any(named: 'args'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('verifies artifacts exist and returns app path', () async {
|
||||
expect(
|
||||
await runWithOverrides(releaser.buildReleaseArtifacts),
|
||||
|
||||
@@ -107,6 +107,9 @@ void main() {
|
||||
when(() => argResults['platforms']).thenReturn(['android']);
|
||||
when(() => argResults['flutter-version']).thenReturn('latest');
|
||||
when(() => argResults.wasParsed(any())).thenReturn(true);
|
||||
when(
|
||||
() => argResults.wasParsed(CommonArguments.publicKeyCmd.name),
|
||||
).thenReturn(false);
|
||||
|
||||
when(cache.updateAll).thenAnswer((_) async => {});
|
||||
|
||||
@@ -699,7 +702,9 @@ $exception'''),
|
||||
verify(
|
||||
() => logger.warn(
|
||||
'patch_verification is set in shorebird.yaml but '
|
||||
'--${CommonArguments.publicKeyArg.name} was not provided.\n'
|
||||
'no public key was provided '
|
||||
'(--${CommonArguments.publicKeyArg.name} '
|
||||
'or --${CommonArguments.publicKeyCmd.name}).\n'
|
||||
'patch_verification configuration will have no effect.',
|
||||
),
|
||||
).called(1);
|
||||
|
||||
@@ -297,6 +297,12 @@ To change the version of this release, change your app's version in your pubspec
|
||||
|
||||
group('when public key is passed as an arg', () {
|
||||
setUp(() {
|
||||
final publicKeyFile = File(
|
||||
p.join(
|
||||
Directory.systemTemp.createTempSync().path,
|
||||
'public-key.pem',
|
||||
),
|
||||
)..createSync(recursive: true);
|
||||
when(
|
||||
() => artifactBuilder.buildWindowsApp(
|
||||
target: any(named: 'target'),
|
||||
@@ -309,9 +315,9 @@ To change the version of this release, change your app's version in your pubspec
|
||||
).thenReturn(true);
|
||||
when(
|
||||
() => argResults[CommonArguments.publicKeyArg.name],
|
||||
).thenReturn('public_key');
|
||||
).thenReturn(publicKeyFile.path);
|
||||
when(
|
||||
() => codeSigner.base64PublicKey(any()),
|
||||
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||
).thenReturn('encoded_public_key');
|
||||
});
|
||||
|
||||
@@ -326,6 +332,45 @@ To change the version of this release, change your app's version in your pubspec
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('when a public-key-cmd is provided', () {
|
||||
setUp(() {
|
||||
when(
|
||||
() => artifactBuilder.buildWindowsApp(
|
||||
target: any(named: 'target'),
|
||||
args: any(named: 'args'),
|
||||
base64PublicKey: any(named: 'base64PublicKey'),
|
||||
),
|
||||
).thenAnswer((_) async => projectRoot);
|
||||
when(
|
||||
() => argResults[CommonArguments.publicKeyCmd.name],
|
||||
).thenReturn('get-key-cmd');
|
||||
when(
|
||||
() => argResults.wasParsed(CommonArguments.publicKeyCmd.name),
|
||||
).thenReturn(true);
|
||||
|
||||
when(
|
||||
() => codeSigner.runPublicKeyCmd(any()),
|
||||
).thenAnswer((_) async => 'pem-public-key');
|
||||
when(
|
||||
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||
).thenReturn('encoded_public_key_from_cmd');
|
||||
});
|
||||
|
||||
test('passes public key to buildWindowsApp', () async {
|
||||
await runWithOverrides(releaser.buildReleaseArtifacts);
|
||||
verify(
|
||||
() => codeSigner.runPublicKeyCmd('get-key-cmd'),
|
||||
).called(1);
|
||||
verify(
|
||||
() => artifactBuilder.buildWindowsApp(
|
||||
base64PublicKey: 'encoded_public_key_from_cmd',
|
||||
target: any(named: 'target'),
|
||||
args: any(named: 'args'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('getReleaseVersion', () {
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
// cspell:ignore qwer
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:scoped_deps/scoped_deps.dart';
|
||||
import 'package:shorebird_cli/src/code_signer.dart';
|
||||
import 'package:shorebird_cli/src/common_arguments.dart';
|
||||
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
||||
import 'package:shorebird_cli/src/logging/logging.dart';
|
||||
import 'package:shorebird_cli/src/release_type.dart';
|
||||
import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class MockCodeSigner extends Mock implements CodeSigner {}
|
||||
|
||||
class MockShorebirdLogger extends Mock implements ShorebirdLogger {}
|
||||
|
||||
class FakeFile extends Fake implements File {}
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
registerFallbackValue(FakeFile());
|
||||
});
|
||||
|
||||
group('OptionFinder', () {
|
||||
late ArgParser argParser;
|
||||
|
||||
@@ -300,4 +318,225 @@ void main() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('CodeSign', () {
|
||||
final cryptoFixturesBasePath = p.join('test', 'fixtures', 'crypto');
|
||||
final publicKeyFile = File(p.join(cryptoFixturesBasePath, 'public.pem'));
|
||||
|
||||
late ArgParser parser;
|
||||
late ShorebirdLogger logger;
|
||||
|
||||
setUp(() {
|
||||
logger = MockShorebirdLogger();
|
||||
parser = ArgParser()
|
||||
..addOption(CommonArguments.publicKeyArg.name)
|
||||
..addOption(CommonArguments.privateKeyArg.name)
|
||||
..addOption(CommonArguments.publicKeyCmd.name)
|
||||
..addOption(CommonArguments.signCmd.name);
|
||||
});
|
||||
|
||||
group('assertAbsentOrValidKeyPairOrCommands', () {
|
||||
test('succeeds when no signing arguments provided', () {
|
||||
final args = <String>[];
|
||||
final result = parser.parse(args);
|
||||
expect(result.assertAbsentOrValidKeyPairOrCommands, returnsNormally);
|
||||
});
|
||||
|
||||
test('throws when both public key sources provided', () {
|
||||
final args = [
|
||||
'--${CommonArguments.publicKeyArg.name}=${publicKeyFile.path}',
|
||||
'--${CommonArguments.publicKeyCmd.name}=get-key-cmd',
|
||||
'--${CommonArguments.signCmd.name}=sign-cmd',
|
||||
];
|
||||
final result = parser.parse(args);
|
||||
|
||||
runScoped(
|
||||
() {
|
||||
expect(
|
||||
result.assertAbsentOrValidKeyPairOrCommands,
|
||||
throwsA(isA<ProcessExit>()),
|
||||
);
|
||||
},
|
||||
values: {loggerRef.overrideWith(() => logger)},
|
||||
);
|
||||
});
|
||||
|
||||
test('throws when both signing methods provided', () {
|
||||
final privateKeyFile = File(
|
||||
p.join(cryptoFixturesBasePath, 'private.pem'),
|
||||
);
|
||||
final args = [
|
||||
'--${CommonArguments.publicKeyArg.name}=${publicKeyFile.path}',
|
||||
'--${CommonArguments.privateKeyArg.name}=${privateKeyFile.path}',
|
||||
'--${CommonArguments.signCmd.name}=sign-cmd',
|
||||
];
|
||||
final result = parser.parse(args);
|
||||
|
||||
runScoped(
|
||||
() {
|
||||
expect(
|
||||
result.assertAbsentOrValidKeyPairOrCommands,
|
||||
throwsA(isA<ProcessExit>()),
|
||||
);
|
||||
},
|
||||
values: {loggerRef.overrideWith(() => logger)},
|
||||
);
|
||||
});
|
||||
|
||||
test('throws when sign-cmd provided without any public key', () {
|
||||
final args = ['--${CommonArguments.signCmd.name}=sign-cmd'];
|
||||
final result = parser.parse(args);
|
||||
|
||||
runScoped(
|
||||
() {
|
||||
expect(
|
||||
result.assertAbsentOrValidKeyPairOrCommands,
|
||||
throwsA(isA<ProcessExit>()),
|
||||
);
|
||||
},
|
||||
values: {loggerRef.overrideWith(() => logger)},
|
||||
);
|
||||
});
|
||||
|
||||
test('succeeds when both cmd arguments provided', () {
|
||||
final args = [
|
||||
'--${CommonArguments.publicKeyCmd.name}=get-key-cmd',
|
||||
'--${CommonArguments.signCmd.name}=sign-cmd',
|
||||
];
|
||||
final result = parser.parse(args);
|
||||
expect(result.assertAbsentOrValidKeyPairOrCommands, returnsNormally);
|
||||
});
|
||||
|
||||
test('succeeds with public-key-path + sign-cmd (mixed)', () {
|
||||
final args = [
|
||||
'--${CommonArguments.publicKeyArg.name}=${publicKeyFile.path}',
|
||||
'--${CommonArguments.signCmd.name}=sign-cmd',
|
||||
];
|
||||
final result = parser.parse(args);
|
||||
expect(result.assertAbsentOrValidKeyPairOrCommands, returnsNormally);
|
||||
});
|
||||
|
||||
test('succeeds when both file arguments provided with valid files', () {
|
||||
final privateKeyFile = File(
|
||||
p.join(cryptoFixturesBasePath, 'private.pem'),
|
||||
);
|
||||
final args = [
|
||||
'--${CommonArguments.publicKeyArg.name}=${publicKeyFile.path}',
|
||||
'--${CommonArguments.privateKeyArg.name}=${privateKeyFile.path}',
|
||||
];
|
||||
final result = parser.parse(args);
|
||||
expect(result.assertAbsentOrValidKeyPairOrCommands, returnsNormally);
|
||||
});
|
||||
});
|
||||
|
||||
group('assertAbsentOrValidPublicKeyOrCmd', () {
|
||||
test('succeeds when no public key arguments provided', () {
|
||||
final args = <String>[];
|
||||
final result = parser.parse(args);
|
||||
expect(result.assertAbsentOrValidPublicKeyOrCmd, returnsNormally);
|
||||
});
|
||||
|
||||
test('succeeds when only public-key-path provided', () {
|
||||
final args = [
|
||||
'--${CommonArguments.publicKeyArg.name}=${publicKeyFile.path}',
|
||||
];
|
||||
final result = parser.parse(args);
|
||||
expect(result.assertAbsentOrValidPublicKeyOrCmd, returnsNormally);
|
||||
});
|
||||
|
||||
test('succeeds when only public-key-cmd provided', () {
|
||||
final args = ['--${CommonArguments.publicKeyCmd.name}=get-key-cmd'];
|
||||
final result = parser.parse(args);
|
||||
expect(result.assertAbsentOrValidPublicKeyOrCmd, returnsNormally);
|
||||
});
|
||||
|
||||
test('throws when both public-key-path and public-key-cmd provided', () {
|
||||
final args = [
|
||||
'--${CommonArguments.publicKeyArg.name}=${publicKeyFile.path}',
|
||||
'--${CommonArguments.publicKeyCmd.name}=get-key-cmd',
|
||||
];
|
||||
final result = parser.parse(args);
|
||||
|
||||
runScoped(
|
||||
() {
|
||||
expect(
|
||||
result.assertAbsentOrValidPublicKeyOrCmd,
|
||||
throwsA(isA<ProcessExit>()),
|
||||
);
|
||||
},
|
||||
values: {loggerRef.overrideWith(() => logger)},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('getEncodedPublicKey', () {
|
||||
late CodeSigner codeSigner;
|
||||
|
||||
setUp(() {
|
||||
codeSigner = MockCodeSigner();
|
||||
});
|
||||
|
||||
test('returns null when no public key configured', () async {
|
||||
final args = <String>[];
|
||||
final result = parser.parse(args);
|
||||
|
||||
await runScoped(
|
||||
() async {
|
||||
final encoded = await result.getEncodedPublicKey();
|
||||
expect(encoded, isNull);
|
||||
},
|
||||
values: {codeSignerRef.overrideWith(() => codeSigner)},
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'returns encoded key from file when public-key-path provided',
|
||||
() async {
|
||||
final args = [
|
||||
'--${CommonArguments.publicKeyArg.name}=${publicKeyFile.path}',
|
||||
];
|
||||
final result = parser.parse(args);
|
||||
|
||||
when(
|
||||
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||
).thenReturn('encoded-key');
|
||||
|
||||
await runScoped(
|
||||
() async {
|
||||
final encoded = await result.getEncodedPublicKey();
|
||||
expect(encoded, equals('encoded-key'));
|
||||
},
|
||||
values: {codeSignerRef.overrideWith(() => codeSigner)},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'returns encoded key from cmd when public-key-cmd provided',
|
||||
() async {
|
||||
final args = ['--${CommonArguments.publicKeyCmd.name}=get-key-cmd'];
|
||||
final result = parser.parse(args);
|
||||
|
||||
when(
|
||||
() => codeSigner.runPublicKeyCmd(any()),
|
||||
).thenAnswer((_) async => 'pem-key');
|
||||
when(
|
||||
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||
).thenReturn('encoded-key');
|
||||
|
||||
await runScoped(
|
||||
() async {
|
||||
final encoded = await result.getEncodedPublicKey();
|
||||
expect(encoded, equals('encoded-key'));
|
||||
verify(() => codeSigner.runPublicKeyCmd('get-key-cmd')).called(1);
|
||||
verify(
|
||||
() => codeSigner.base64PublicKeyFromPem('pem-key'),
|
||||
).called(1);
|
||||
},
|
||||
values: {codeSignerRef.overrideWith(() => codeSigner)},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user