jdk/test/jdk/tools/jpackage/share/ErrorTest.java
2026-03-25 02:44:14 +00:00

1355 lines
58 KiB
Java

/*
* Copyright (c) 2020, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import static java.util.stream.Collectors.toMap;
import static jdk.internal.util.OperatingSystem.LINUX;
import static jdk.internal.util.OperatingSystem.MACOS;
import static jdk.internal.util.OperatingSystem.WINDOWS;
import static jdk.jpackage.internal.util.PListWriter.writePList;
import static jdk.jpackage.internal.util.XmlUtils.createXml;
import static jdk.jpackage.internal.util.XmlUtils.toXmlConsumer;
import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction;
import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier;
import static jdk.jpackage.test.JPackageCommand.makeAdvice;
import static jdk.jpackage.test.JPackageCommand.makeError;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import jdk.internal.util.OperatingSystem;
import jdk.jpackage.internal.util.MacBundle;
import jdk.jpackage.internal.util.TokenReplace;
import jdk.jpackage.test.Annotations.Parameter;
import jdk.jpackage.test.Annotations.ParameterSupplier;
import jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.ApplicationLayout;
import jdk.jpackage.test.CannedArgument;
import jdk.jpackage.test.CannedFormattedString;
import jdk.jpackage.test.JPackageCommand;
import jdk.jpackage.test.JPackageOutputValidator;
import jdk.jpackage.test.JavaTool;
import jdk.jpackage.test.MacSign;
import jdk.jpackage.test.MacSign.CertificateRequest;
import jdk.jpackage.test.MacSign.CertificateType;
import jdk.jpackage.test.MacSign.KeychainWithCertsSpec;
import jdk.jpackage.test.MacSign.ResolvedKeychain;
import jdk.jpackage.test.MacSign.StandardCertificateNamePrefix;
import jdk.jpackage.test.PackageType;
import jdk.jpackage.test.TKit;
import jdk.jpackage.test.mock.Script;
import jdk.jpackage.test.mock.VerbatimCommandMock;
import jdk.jpackage.test.stdmock.JPackageMockUtils;
import jdk.jpackage.test.stdmock.MacSignMockUtils;
/*
* @test
* @summary Test jpackage output for erroneous input
* @library /test/jdk/tools/jpackage/helpers
* @library /test/lib
* @build jdk.jpackage.test.*
* @build jdk.jpackage.test.stdmock.*
* @build jdk.test.lib.security.CertificateBuilder
* @compile -Xlint:all -Werror ErrorTest.java
* @run main/othervm/timeout=720 -Xmx512m jdk.jpackage.test.Main
* --jpt-run=ErrorTest
* --jpt-before-run=jdk.jpackage.test.JPackageCommand.useExecutableByDefault
*/
/*
* @test
* @summary Test jpackage output for erroneous input
* @library /test/jdk/tools/jpackage/helpers
* @library /test/lib
* @build jdk.jpackage.test.*
* @build jdk.jpackage.test.stdmock.*
* @build jdk.test.lib.security.CertificateBuilder
* @compile -Xlint:all -Werror ErrorTest.java
* @run main/othervm/timeout=720 -Xmx512m jdk.jpackage.test.Main
* --jpt-run=ErrorTest
* --jpt-before-run=jdk.jpackage.test.JPackageCommand.useToolProviderByDefault
*/
public final class ErrorTest {
enum Token {
JAVA_HOME(() -> {
return System.getProperty("java.home");
}),
APP_IMAGE(() -> {
final var appImageRoot = TKit.createTempDirectory("appimage");
final var appImageCmd = JPackageCommand.helloAppImage()
// Use the default jpackage tool provider to create an application image.
// The ErrorTest is used from the OptionsValidationFailTest unit tests that override
// the default jpackage tool provider with the implementation that doesn't do packaging
// and can not create a valid application image.
.useToolProvider(JavaTool.JPACKAGE.asToolProvider())
.setFakeRuntime().setArgumentValue("--dest", appImageRoot);
appImageCmd.execute();
return appImageCmd.outputBundle();
}),
APP_IMAGE_WITH_SHORT_NAME(() -> {
final var appImageRoot = TKit.createTempDirectory("appimage");
final var appImageCmd = JPackageCommand.helloAppImage()
.setFakeRuntime().setArgumentValue("--dest", appImageRoot);
// Let jpackage pick the name from the main class (Hello). It qualifies as the "short" name.
appImageCmd.removeArgumentWithValue("--name");
appImageCmd.execute();
return appImageCmd.outputBundle();
}),
MAC_APP_IMAGE_INVALID_INFO_PLIST(toFunction(cmd -> {
var appImageDir = (Path)APP_IMAGE.expand(cmd).orElseThrow();
// Replace the default Info.plist file with an empty one.
var plistFile = new MacBundle(appImageDir).infoPlistFile();
TKit.trace(String.format("Create invalid plist file [%s]", plistFile));
createXml(plistFile, xml -> {
writePList(xml, toXmlConsumer(() -> {
}));
});
return appImageDir;
})),
INVALID_MAC_RUNTIME_BUNDLE(toSupplier(() -> {
// Has "Contents/MacOS/libjli.dylib", but missing "Contents/Home/lib/libjli.dylib".
final Path root = TKit.createTempDirectory("mac-invalid-runtime-bundle");
Files.createDirectories(root.resolve("Contents/Home"));
Files.createFile(root.resolve("Contents/Info.plist"));
Files.createDirectories(root.resolve("Contents/MacOS"));
Files.createFile(root.resolve("Contents/MacOS/libjli.dylib"));
return root;
})),
INVALID_MAC_RUNTIME_IMAGE(toSupplier(() -> {
// Has some files in the "lib" subdirectory, but doesn't have the "lib/libjli.dylib" file.
final Path root = TKit.createTempDirectory("mac-invalid-runtime-image");
Files.createDirectories(root.resolve("lib"));
Files.createFile(root.resolve("lib/foo"));
return root;
})),
EMPTY_DIR(() -> {
return TKit.createTempDirectory("empty-dir");
}),
ADD_LAUNCHER_PROPERTY_FILE,
EMPTY_KEYCHAIN,
KEYCHAIN_WITH_APP_IMAGE_CERT,
KEYCHAIN_WITH_PKG_CERT,
;
private Token() {
this.valueSupplier = Optional.empty();
}
private Token(Function<JPackageCommand, Object> valueSupplier) {
this.valueSupplier = Optional.of(valueSupplier);
}
private Token(Supplier<Object> valueSupplier) {
this(_ -> {
return valueSupplier.get();
});
}
String token() {
return makeToken(name());
}
TokenReplace asTokenReplace() {
return tokenReplace;
}
Optional<Object> expand(JPackageCommand cmd) {
return valueSupplier.map(func -> func.apply(cmd));
}
private static String makeToken(String v) {
Objects.requireNonNull(v);
return String.format("@@%s@@", v);
}
private final Optional<Function<JPackageCommand, Object>> valueSupplier;
private final TokenReplace tokenReplace = new TokenReplace(token());
}
record PackageTypeSpec(Optional<PackageType> type, boolean anyNativeType) implements CannedArgument {
PackageTypeSpec {
Objects.requireNonNull(type);
if (type.isPresent() && anyNativeType) {
throw new IllegalArgumentException();
}
}
PackageTypeSpec(PackageType type) {
this(Optional.of(type), false);
}
boolean isSupported() {
if (anyNativeType) {
return NATIVE_TYPE.isPresent();
} else {
return type.orElseThrow().isSupported();
}
}
PackageType resolvedType() {
return type.or(() -> NATIVE_TYPE).orElseThrow(PackageType::throwSkippedExceptionIfNativePackagingUnavailable);
}
@Override
public String getValue() {
return resolvedType().getType();
}
@Override
public final String toString() {
if (anyNativeType) {
return "NATIVE";
} else {
return type.orElseThrow().toString();
}
}
private static Optional<PackageType> defaultNativeType() {
final Collection<PackageType> nativeTypes;
if (TKit.isLinux()) {
nativeTypes = PackageType.LINUX;
} else if (TKit.isOSX()) {
nativeTypes = PackageType.MAC;
} else if (TKit.isWindows()) {
nativeTypes = List.of(PackageType.WIN_MSI);
} else {
throw TKit.throwUnknownPlatformError();
}
return nativeTypes.stream().filter(PackageType::isSupported).findFirst();
}
static final PackageTypeSpec NATIVE = new PackageTypeSpec(Optional.empty(), true);
private static final Optional<PackageType> NATIVE_TYPE = defaultNativeType();
}
public record TestSpec(
Optional<PackageTypeSpec> type,
Optional<String> appDesc,
List<String> addArgs,
List<String> removeArgs,
List<CannedFormattedString> expectedMessages,
boolean match) {
static final class Builder {
Builder() {
type = new PackageTypeSpec(PackageType.IMAGE);
appDesc = DEFAULT_APP_DESC;
match = true;
}
Builder(Builder other) {
type = other.type;
appDesc = other.appDesc;
match = other.match;
addArgs.addAll(other.addArgs);
removeArgs.addAll(other.removeArgs);
expectedMessages.addAll(other.expectedMessages);
}
Builder copy() {
return new Builder(this);
}
Builder type(PackageType v) {
type = Optional.ofNullable(v).map(PackageTypeSpec::new).orElse(null);
return this;
}
Builder notype() {
return type(null);
}
Builder nativeType() {
type = PackageTypeSpec.NATIVE;
return this;
}
Builder appDesc(String v) {
appDesc = v;
return this;
}
Builder noAppDesc() {
return appDesc(null);
}
Builder match(boolean v) {
match = v;
return this;
}
Builder match() {
return match(true);
}
Builder find() {
return match(false);
}
Builder setAddArgs(List<String> v) {
addArgs.clear();
addArgs.addAll(v);
return this;
}
Builder setAddArgs(String... v) {
return setAddArgs(List.of(v));
}
Builder addArgs(List<String> v) {
addArgs.addAll(v);
return this;
}
Builder addArgs(String... v) {
return addArgs(List.of(v));
}
Builder setRemoveArgs(List<String> v) {
removeArgs.clear();
removeArgs.addAll(v);
return this;
}
Builder setRemoveArgs(String... v) {
return setRemoveArgs(List.of(v));
}
Builder removeArgs(List<String> v) {
removeArgs.addAll(v);
return this;
}
Builder removeArgs(String... v) {
return removeArgs(List.of(v));
}
Builder setMessages(List<CannedFormattedString> v) {
expectedMessages.clear();
expectedMessages.addAll(v);
return this;
}
Builder setMessages(CannedFormattedString... v) {
return setMessages(List.of(v));
}
Builder messages(List<CannedFormattedString> v) {
expectedMessages.addAll(v);
return this;
}
Builder messages(CannedFormattedString... v) {
return messages(List.of(v));
}
Builder error(String key, Object ... args) {
return messages(makeError(key, args));
}
Builder advice(String key, Object ... args) {
return messages(makeAdvice(key, args));
}
Builder invalidTypeArg(String arg, String... otherArgs) {
Objects.requireNonNull(type);
return addArgs(arg).addArgs(otherArgs).error("ERR_InvalidTypeOption", arg, type);
}
Builder unsupportedPlatformOption(String arg, String ... otherArgs) {
return addArgs(arg).addArgs(otherArgs).error("ERR_UnsupportedOption", arg);
}
TestSpec create() {
return new TestSpec(
Optional.ofNullable(type),
Optional.ofNullable(appDesc),
List.copyOf(addArgs),
List.copyOf(removeArgs),
List.copyOf(expectedMessages),
match);
}
private PackageTypeSpec type;
private String appDesc;
private boolean match;
private final List<String> addArgs = new ArrayList<>();
private final List<String> removeArgs = new ArrayList<>();
private final List<CannedFormattedString> expectedMessages = new ArrayList<>();
}
public TestSpec {
Objects.requireNonNull(type);
Objects.requireNonNull(appDesc);
Objects.requireNonNull(addArgs);
addArgs.forEach(Objects::requireNonNull);
Objects.requireNonNull(removeArgs);
removeArgs.forEach(Objects::requireNonNull);
if (expectedMessages.isEmpty()) {
throw new IllegalArgumentException("The list of expected errors must be non-empty");
}
}
void test() {
test(Map.of());
}
boolean isSupported() {
return type.map(PackageTypeSpec::isSupported).orElse(true);
}
void test(Map<Token, Function<JPackageCommand, Object>> tokenValueSuppliers) {
final var cmd = appDesc.map(JPackageCommand::helloAppImage).orElseGet(JPackageCommand::new);
type.map(PackageTypeSpec::resolvedType).ifPresent(cmd::setPackageType);
removeArgs.forEach(cmd::removeArgumentWithValue);
cmd.addArguments(addArgs);
final var tokenValueSupplier = TokenReplace.createCachingTokenValueSupplier(Stream.of(Token.values()).collect(toMap(Token::token, token -> {
return () -> {
return token.expand(cmd).orElseGet(() -> {
final var tvs = Objects.requireNonNull(tokenValueSuppliers.get(token), () -> {
return String.format("No token value supplier for token [%s]", token);
});
return tvs.apply(cmd);
});
};
})));
for (final var token : Token.values()) {
final var newArgs = cmd.getAllArguments().stream().map(arg -> {
return token.asTokenReplace().applyTo(arg, tokenValueSupplier);
}).toList();
cmd.clearArguments().addArguments(newArgs);
}
// Disable default logic adding `--verbose` option
// to jpackage command line.
// It will affect jpackage error messages if the command line is malformed.
cmd.ignoreDefaultVerbose(true);
// Ignore external runtime as it will interfere
// with jpackage arguments in this test.
cmd.ignoreDefaultRuntime(true);
var validator = new JPackageOutputValidator().stderr().expectMatchingStrings(expectedMessages).match(match);
if (match) {
new JPackageOutputValidator().stdout().validateEndOfStream().applyTo(cmd);
}
cmd.mutate(validator::applyTo).execute(1);
}
TestSpec mapExpectedMessages(UnaryOperator<CannedFormattedString> mapper) {
return new TestSpec(type, appDesc, addArgs, removeArgs, expectedMessages.stream().map(mapper).toList(), match);
}
TestSpec copyWithExpectedMessages(List<CannedFormattedString> expectedMessages) {
return new TestSpec(type, appDesc, addArgs, removeArgs, expectedMessages, match);
}
@Override
public final String toString() {
final var sb = new StringBuilder();
type.ifPresent(v -> {
sb.append(v).append("; ");
});
appDesc.ifPresent(v -> {
sb.append("app-desc=").append(v).append("; ");
});
if (!addArgs.isEmpty()) {
sb.append("args-add=").append(addArgs).append("; ");
}
if (!removeArgs.isEmpty()) {
sb.append("args-del=").append(removeArgs).append("; ");
}
if (!match) {
sb.append("find; ");
}
sb.append("errors=").append(expectedMessages);
return sb.toString();
}
private static final String DEFAULT_APP_DESC = "Hello";
}
private static TestSpec.Builder testSpec() {
return new TestSpec.Builder();
}
public static Collection<Object[]> basic() {
final List<TestSpec> testCases = new ArrayList<>();
testCases.addAll(Stream.of(
// non-existent arg
testSpec().addArgs("--no-such-argument")
.error("ERR_InvalidOption", "--no-such-argument"),
// no main jar
testSpec().removeArgs("--main-jar").error("ERR_NoEntryPoint"),
// no main-class
testSpec().removeArgs("--main-class")
.error("error.no-main-class-with-main-jar", "hello.jar")
.advice("error.no-main-class-with-main-jar.advice", "hello.jar"),
// non-existent main jar
testSpec().addArgs("--main-jar", "non-existent.jar").find()
.error("error.main-jar-does-not-exist", "non-existent.jar"),
// non-existent runtime
testSpec().addArgs("--runtime-image", "non-existent.runtime")
.error("error.parameter-not-directory", "non-existent.runtime", "--runtime-image"),
// non-existent app image
testSpec().noAppDesc().nativeType().addArgs("--name", "foo", "--app-image", "non-existent.appimage")
.error("error.parameter-not-directory", "non-existent.appimage", "--app-image"),
// non-existent resource-dir
testSpec().addArgs("--resource-dir", "non-existent.dir")
.error("error.parameter-not-directory", "non-existent.dir", "--resource-dir"),
// non-existent icon
testSpec().addArgs("--icon", "non-existent.icon")
.error("error.parameter-not-file", "non-existent.icon", "--icon"),
// non-existent license file
testSpec().nativeType().addArgs("--license-file", "non-existent.license")
.error("error.parameter-not-file", "non-existent.license", "--license-file"),
// invalid type
testSpec().addArgs("--type", "invalid-type")
.error("ERR_InvalidInstallerType", "invalid-type"),
// no --input for non-mudular app
testSpec().removeArgs("--input").error("error.no-input-parameter"),
// no --module-path
testSpec().appDesc("com.other/com.other.Hello").removeArgs("--module-path")
.error("ERR_MissingArgument2", "--runtime-image", "--module-path"),
// no main class in module path
testSpec().noAppDesc().addArgs("--module", "java.base", "--runtime-image", Token.JAVA_HOME.token())
.error("ERR_NoMainClass"),
// no module in module path
testSpec().noAppDesc().addArgs("--module", "com.foo.bar", "--runtime-image", Token.JAVA_HOME.token())
.error("error.no-module-in-path", "com.foo.bar"),
// non-existing argument file
testSpec().noAppDesc().notype().addArgs("@foo")
.error("ERR_CannotParseOptions", "foo")
).map(TestSpec.Builder::create).toList());
// --main-jar and --module-name
createMutuallyExclusive(
new ArgumentGroup("--module", "foo.bar"),
new ArgumentGroup("--main-jar", "foo.jar")
).map(TestSpec.Builder::noAppDesc).map(TestSpec.Builder::find).map(TestSpec.Builder::create).forEach(testCases::add);
// forbidden jlink options
testCases.addAll(Stream.of("--output", "--add-modules", "--module-path").map(opt -> {
return testSpec().addArgs("--jlink-options", opt).error("error.blocked.option", opt);
}).map(TestSpec.Builder::create).toList());
// --runtime-image and --app-image are mutually-exclusive
testCases.addAll(createRuntimeMutuallyExclusive("--app-image", "app-image"));
// --runtime-image and --app-modules are mutually-exclusive
testCases.addAll(createRuntimeMutuallyExclusive("--add-modules", "foo.bar", "--module", "foo.bar"));
// --runtime-image and --jlink-options are mutually-exclusive
testCases.addAll(createRuntimeMutuallyExclusive("--jlink-options", "--bind-services", "--module", "foo.bar"));
return toTestArgs(testCases.stream());
}
record ArgumentGroup(String arg, String... otherArgs) {
ArgumentGroup {
Objects.requireNonNull(arg);
List.of(otherArgs).forEach(Objects::requireNonNull);
}
String[] asArray() {
return Stream.concat(Stream.of(arg), Stream.of(otherArgs)).toArray(String[]::new);
}
}
private static List<TestSpec> createRuntimeMutuallyExclusive(String arg, String... otherArgs) {
return createMutuallyExclusive(
new ArgumentGroup("--runtime-image", Token.JAVA_HOME.token()),
new ArgumentGroup(arg, otherArgs)
).map(TestSpec.Builder::noAppDesc).map(TestSpec.Builder::nativeType).map(TestSpec.Builder::create).toList();
}
private static Stream<TestSpec.Builder> createMutuallyExclusive(ArgumentGroup firstGroup, ArgumentGroup secondGroup) {
final Supplier<TestSpec.Builder> createBuilder = () -> {
return testSpec().error("ERR_MutuallyExclusiveOptions", firstGroup.arg(), secondGroup.arg());
};
return Stream.of(
createBuilder.get().addArgs(firstGroup.asArray()).addArgs(secondGroup.asArray()),
createBuilder.get().addArgs(secondGroup.asArray()).addArgs(firstGroup.asArray()));
}
public static Collection<Object[]> invalidAppVersion() {
return toTestArgs(Stream.of(
// Invalid app version. Just cover all different error messages.
// Extensive testing of invalid version strings is done in DottedVersionTest unit test.
testSpec().addArgs("--app-version", "").error("error.version-string-empty"),
testSpec().addArgs("--app-version", "1.").error("error.version-string-zero-length-component", "1."),
testSpec().addArgs("--app-version", "1.b.3").error("error.version-string-invalid-component", "1.b.3", "b.3")
).map(builder -> {
if (TKit.isOSX()) {
builder.advice("error.invalid-cfbundle-version.advice");
};
return builder;
}));
}
@Test
@ParameterSupplier("basic")
@ParameterSupplier("testRuntimeInstallerInvalidOptions")
@ParameterSupplier(value="testWindows", ifOS = WINDOWS)
@ParameterSupplier(value="testMac", ifOS = MACOS)
@ParameterSupplier(value="testLinux", ifOS = LINUX)
@ParameterSupplier(value="winOption", ifNotOS = WINDOWS)
@ParameterSupplier(value="linuxOption", ifNotOS = LINUX)
@ParameterSupplier(value="macOption", ifNotOS = MACOS)
@ParameterSupplier(value="invalidAppVersion", ifOS = {WINDOWS,MACOS})
public static void test(TestSpec spec) {
spec.test();
}
public static Collection<Object[]> testRuntimeInstallerInvalidOptions() {
Stream<List<String>> argsStream = Stream.of(
List.of("--input", "foo"),
List.of("--module-path", "dir"),
List.of("--add-modules", "java.base"),
List.of("--main-class", "Hello"),
List.of("--arguments", "foo"),
List.of("--java-options", "-Dfoo.bar=10"),
List.of("--add-launcher", "foo=foo.properties"),
List.of("--app-content", "dir"));
if (TKit.isWindows()) {
argsStream = Stream.concat(argsStream, Stream.of(List.of("--win-console")));
}
return toTestArgs(argsStream.map(args -> {
var builder = testSpec().noAppDesc().nativeType()
.addArgs("--runtime-image", Token.JAVA_HOME.token())
.addArgs(args);
if (args.contains("--add-modules")) {
builder.error("ERR_MutuallyExclusiveOptions", "--runtime-image", "--add-modules");
}
return builder.error("ERR_NoInstallerEntryPoint", args.getFirst());
}));
}
@Test
@ParameterSupplier
public static void testAdditionLaunchers(TestSpec spec) {
final Path propsFile = TKit.createTempFile("add-launcher.properties");
TKit.createPropertiesFile(propsFile, Map.of());
spec.mapExpectedMessages(cannedStr -> {
return cannedStr.mapArgs(arg -> {
if (arg == Token.ADD_LAUNCHER_PROPERTY_FILE) {
return propsFile;
} else {
return arg;
}
});
}).test(Map.of(Token.ADD_LAUNCHER_PROPERTY_FILE, cmd -> propsFile));
}
public static Collection<Object[]> testAdditionLaunchers() {
return toTestArgs(Stream.of(
testSpec().addArgs("--add-launcher", Token.ADD_LAUNCHER_PROPERTY_FILE.token())
.error("error.parameter-add-launcher-malformed", Token.ADD_LAUNCHER_PROPERTY_FILE, "--add-launcher"),
testSpec().removeArgs("--name").addArgs("--name", "foo", "--add-launcher", "foo=" + Token.ADD_LAUNCHER_PROPERTY_FILE.token())
.error("error.launcher-duplicate-name", "foo")
));
}
@Test(ifOS = MACOS)
public static void testMacSignAppStoreInvalidRuntime() throws IOException {
// Create app image with the runtime directory content that will fail the subsequent signing jpackage command.
var appImageCmd = JPackageCommand.helloAppImage().setFakeRuntime();
appImageCmd.executeAndAssertImageCreated();
Files.createDirectory(appImageCmd.appLayout().runtimeHomeDirectory().resolve("bin"));
final var keychain = SignEnvMock.SingleCertificateKeychain.FOO.keychain();
var spec = testSpec()
.noAppDesc()
.addArgs("--mac-app-store", "--mac-sign", "--app-image", appImageCmd.outputBundle().toString())
.error("error.invalid-app-image-runtime-image-bin-dir",
ApplicationLayout.macAppImage().runtimeHomeDirectory(), appImageCmd.outputBundle())
.create();
TKit.withNewState(() -> {
var script = Script.build()
// Disable the mutation making mocks "run once".
.commandMockBuilderMutator(null)
// Replace "/usr/bin/security" with the mock bound to the keychain mock.
.map(MacSignMockUtils.securityMock(SignEnvMock.VALUE))
// Don't mock other external commands.
.use(VerbatimCommandMock.INSTANCE)
.createLoop();
// Create jpackage tool provider using the /usr/bin/security mock.
var jpackage = JPackageMockUtils.createJPackageToolProvider(OperatingSystem.MACOS, script);
// Override the default jpackage tool provider with the one using the /usr/bin/security mock.
JPackageCommand.useToolProviderByDefault(jpackage);
spec.test();
});
}
@Test(ifOS = MACOS)
@ParameterSupplier
@ParameterSupplier("testMacPkgSignWithoutIdentity")
public static void testMacSignWithoutIdentity(TestSpec spec) {
// The test calls JPackageCommand.useToolProviderByDefault(),
// which alters global variables in the test library,
// so run the test case with a new global state to isolate the alteration of the globals.
TKit.withNewState(() -> {
testMacSignWithoutIdentityWithNewTKitState(spec);
});
}
private static void testMacSignWithoutIdentityWithNewTKitState(TestSpec spec) {
final Token keychainToken = spec.expectedMessages().stream().flatMap(cannedStr -> {
return cannedStr.args().stream().filter(Token.class::isInstance).map(Token.class::cast).filter(token -> {
switch (token) {
case EMPTY_KEYCHAIN, KEYCHAIN_WITH_APP_IMAGE_CERT, KEYCHAIN_WITH_PKG_CERT -> {
return true;
}
default -> {
return false;
}
}
});
}).distinct().reduce((a, b) -> {
throw new IllegalStateException(String.format(
"Error messages %s reference multiple keychains: %s and %s", spec.expectedMessages(), a, b));
}).orElseThrow();
final ResolvedKeychain keychain;
switch (keychainToken) {
case EMPTY_KEYCHAIN -> {
keychain = new ResolvedKeychain(new KeychainWithCertsSpec(MacSign.createEmptyKeychain(), List.of()));
}
case KEYCHAIN_WITH_APP_IMAGE_CERT, KEYCHAIN_WITH_PKG_CERT -> {
CertificateType existingCertType;
switch (keychainToken) {
case KEYCHAIN_WITH_APP_IMAGE_CERT -> {
existingCertType = CertificateType.CODE_SIGN;
}
case KEYCHAIN_WITH_PKG_CERT -> {
existingCertType = CertificateType.INSTALLER;
}
default -> {
throw new AssertionError();
}
}
keychain = Stream.of(SignEnvMock.SingleCertificateKeychain.values()).filter(k -> {
return k.certificateType() == existingCertType;
}).findFirst().orElseThrow().keychain();
var script = Script.build()
// Disable the mutation making mocks "run once".
.commandMockBuilderMutator(null)
// Replace "/usr/bin/security" with the mock bound to the keychain mock.
.map(MacSignMockUtils.securityMock(SignEnvMock.VALUE))
// Don't mock other external commands.
.use(VerbatimCommandMock.INSTANCE)
.createLoop();
// Create jpackage tool provider using the /usr/bin/security mock.
var jpackage = JPackageMockUtils.createJPackageToolProvider(OperatingSystem.MACOS, script);
// Override the default jpackage tool provider with the one using the /usr/bin/security mock.
JPackageCommand.useToolProviderByDefault(jpackage);
}
default -> {
throw new AssertionError();
}
}
MacSign.withKeychain(_ -> {
spec.mapExpectedMessages(cannedStr -> {
return cannedStr.mapArgs(arg -> {
switch (arg) {
case StandardCertificateNamePrefix certPrefix -> {
return certPrefix.value();
}
case Token _ -> {
return keychain.name();
}
default -> {
return arg;
}
}
});
}).test(Map.of(keychainToken, _ -> keychain.name()));
}, keychain);
}
public static Collection<Object[]> testMacSignWithoutIdentity() {
final List<TestSpec> testCases = new ArrayList<>();
final var signArgs = List.of("--mac-sign", "--mac-signing-keychain", Token.EMPTY_KEYCHAIN.token());
final var appImageArgs = List.of("--app-image", Token.APP_IMAGE_WITH_SHORT_NAME.token());
for (var withAppImage : List.of(true, false)) {
var builder = testSpec();
if (withAppImage) {
builder.noAppDesc().addArgs(appImageArgs);
}
builder.addArgs(signArgs);
for (var type: List.of(PackageType.IMAGE, PackageType.MAC_PKG, PackageType.MAC_DMG)) {
builder.setMessages().error("error.cert.not.found",
MacSign.StandardCertificateNamePrefix.CODE_SIGN, Token.EMPTY_KEYCHAIN);
switch (type) {
case MAC_PKG -> {
// jpackage must report two errors:
// 1. It can't find signing identity to sign the app image
// 2. It can't find signing identity to sign the PKG installer
builder.error("error.cert.not.found",
MacSign.StandardCertificateNamePrefix.INSTALLER, Token.EMPTY_KEYCHAIN);
}
default -> {
// NOP
}
}
var testSpec = builder.type(type).create();
testCases.add(testSpec);
}
}
return toTestArgs(testCases);
}
public static Collection<Object[]> testMacPkgSignWithoutIdentity() {
final List<TestSpec.Builder> testCases = new ArrayList<>();
final var appImageArgs = List.of("--app-image", Token.APP_IMAGE_WITH_SHORT_NAME.token());
for (var withAppImage : List.of(true, false)) {
for (var existingCertType : CertificateType.values()) {
Token keychain;
StandardCertificateNamePrefix missingCertificateNamePrefix;
switch (existingCertType) {
case INSTALLER -> {
keychain = Token.KEYCHAIN_WITH_PKG_CERT;
missingCertificateNamePrefix = StandardCertificateNamePrefix.CODE_SIGN;
}
case CODE_SIGN -> {
keychain = Token.KEYCHAIN_WITH_APP_IMAGE_CERT;
missingCertificateNamePrefix = StandardCertificateNamePrefix.INSTALLER;
}
default -> {
throw new AssertionError();
}
}
var builder = testSpec()
.type(PackageType.MAC_PKG)
.addArgs("--mac-sign", "--mac-signing-keychain", keychain.token())
.error("error.cert.not.found", missingCertificateNamePrefix, keychain);
if (withAppImage) {
builder.noAppDesc().addArgs(appImageArgs);
} else {
/*
* Use shorter name to avoid
*
* [03:08:55.623] --mac-package-name is set to 'MacSignWithoutIdentityErrorTest', which is longer than 16 characters. For a better Mac experience consider shortening it.
*
* in the output.
* The same idea is behind using the "APP_IMAGE_WITH_SHORT_NAME" token
* instead of the "APP_IMAGE" for the predefined app image.
*/
builder.removeArgs("--name");
}
testCases.add(builder);
}
}
return toTestArgs(testCases);
}
@Test
@ParameterSupplier("invalidNames")
public static void testInvalidAppName(InvalidName name) {
testSpec().removeArgs("--name").addArgs("--name", name.value())
.error("ERR_InvalidAppName", adjustTextStreamVerifierArg(name.value()))
.match(!name.isMessingUpConsoleOutput())
.create()
.test();
}
@Test
@ParameterSupplier("invalidNames")
public static void testInvalidAddLauncherName(InvalidName name) {
testAdditionLaunchers(testSpec()
.addArgs("--add-launcher", name + "=" + Token.ADD_LAUNCHER_PROPERTY_FILE.token())
.error("ERR_InvalidSLName", adjustTextStreamVerifierArg(name.value()))
.match(!name.isMessingUpConsoleOutput())
.create());
}
public static Collection<Object[]> invalidNames() {
final List<String> data = new ArrayList<>();
data.addAll(List.of("", "foo/bar", "foo\tbar", "foo\rbar", "foo\nbar"));
if (TKit.isWindows()) {
data.add("foo\\bar");
}
return toTestArgs(data.stream().map(InvalidName::new));
}
record InvalidName(String value) {
InvalidName {
Objects.requireNonNull(value);
}
boolean isMessingUpConsoleOutput() {
var controlChars = "\r\n\t".codePoints().toArray();
return value.codePoints().anyMatch(cp -> {
return IntStream.of(controlChars).anyMatch(v -> {
return v == cp;
});
});
}
@Override
public String toString() {
return value;
}
}
public static Collection<Object[]> testWindows() {
final List<TestSpec> testCases = new ArrayList<>();
testCases.addAll(PackageType.WINDOWS.stream().map(type -> {
return Stream.of(
testSpec().type(type).addArgs("--launcher-as-service")
.error("error.missing-service-installer")
.advice("error.missing-service-installer.advice"),
// The below version strings are invalid for msi and exe packaging.
// They are valid for app image packaging.
testSpec().type(type).addArgs("--app-version", "1234")
.error("error.msi-product-version-components", "1234")
.advice("error.version-string-wrong-format.advice"),
testSpec().type(type).addArgs("--app-version", "1.2.3.4.5")
.error("error.msi-product-version-components", "1.2.3.4.5")
.advice("error.version-string-wrong-format.advice"),
testSpec().type(type).addArgs("--app-version", "256.1")
.error("error.msi-product-version-major-out-of-range")
.advice("error.version-string-wrong-format.advice"),
testSpec().type(type).addArgs("--app-version", "1.256")
.error("error.msi-product-version-minor-out-of-range")
.advice("error.version-string-wrong-format.advice"),
testSpec().type(type).addArgs("--app-version", "1.2.65536")
.error("error.msi-product-version-build-out-of-range")
.advice("error.version-string-wrong-format.advice")
);
}).flatMap(x -> x).map(TestSpec.Builder::create).toList());
invalidShortcut(testCases::add, "--win-menu");
invalidShortcut(testCases::add, "--win-shortcut");
return toTestArgs(testCases.stream());
}
public static Collection<Object[]> testMac() {
final List<TestSpec> testCases = new ArrayList<>();
testCases.addAll(Stream.of(
testSpec().addArgs("--app-version", "0.2")
.error("message.version-string-first-number-not-zero")
.advice("error.invalid-cfbundle-version.advice"),
testSpec().addArgs("--app-version", "1.2.3.4")
.error("message.version-string-too-many-components")
.advice("error.invalid-cfbundle-version.advice"),
testSpec().invalidTypeArg("--mac-installer-sign-identity", "foo"),
testSpec().type(PackageType.MAC_DMG).invalidTypeArg("--mac-installer-sign-identity", "foo"),
testSpec().invalidTypeArg("--mac-dmg-content", "foo"),
testSpec().type(PackageType.MAC_PKG).invalidTypeArg("--mac-dmg-content", "foo"),
testSpec().noAppDesc().addArgs("--app-image", Token.APP_IMAGE.token())
.error("error.app-image.mac-sign.required"),
testSpec().type(PackageType.MAC_PKG).addArgs("--mac-package-identifier", "#1")
.error("error.parameter-not-mac-bundle-identifier", "#1", "--mac-package-identifier")
.advice("error.parameter-not-mac-bundle-identifier.advice"),
// Bundle for mac app store should not have runtime commands
testSpec().nativeType().addArgs("--mac-app-store", "--jlink-options", "--bind-services")
.error("ERR_MissingJLinkOptMacAppStore", "--strip-native-commands"),
// Predefined app image must be a valid macOS bundle.
testSpec().noAppDesc().nativeType().addArgs("--app-image", Token.EMPTY_DIR.token())
.error("error.parameter-not-mac-bundle", JPackageCommand.cannedArgument(cmd -> {
return Path.of(cmd.getArgumentValue("--app-image"));
}, Token.EMPTY_DIR.token()), "--app-image"),
testSpec().nativeType().noAppDesc().addArgs("--app-image", Token.MAC_APP_IMAGE_INVALID_INFO_PLIST.token())
.error("error.invalid-app-image-plist-file", JPackageCommand.cannedArgument(cmd -> {
return new MacBundle(Path.of(cmd.getArgumentValue("--app-image"))).infoPlistFile();
}, Token.MAC_APP_IMAGE_INVALID_INFO_PLIST.token()))
).map(TestSpec.Builder::create).toList());
macInvalidRuntime(testCases::add);
// Test a few app-image options that should not be used when signing external app image
testCases.addAll(Stream.of(
new ArgumentGroup("--app-version", "2.0"),
new ArgumentGroup("--name", "foo")
).flatMap(argGroup -> {
var withoutSign = testSpec()
.noAppDesc()
.addArgs(argGroup.asArray())
.addArgs("--app-image", Token.APP_IMAGE.token());
var withSign = withoutSign.copy().addArgs("--mac-sign");
withoutSign.error("error.app-image.mac-sign.required");
// It should bail out with the same error message regardless of `--mac-sign` option.
return Stream.of(withoutSign, withSign).map(builder -> {
return builder.error("ERR_InvalidOptionWithAppImageSigning", argGroup.arg());
});
}).map(TestSpec.Builder::create).toList());
testCases.addAll(createMutuallyExclusive(
new ArgumentGroup("--mac-signing-key-user-name", "foo"),
new ArgumentGroup("--mac-app-image-sign-identity", "bar")
).mapMulti(ErrorTest::duplicateForMacSign).toList());
for (var packageType : PackageType.MAC) {
testCases.addAll(createMutuallyExclusive(
new ArgumentGroup("--mac-signing-key-user-name", "foo"),
new ArgumentGroup("--mac-installer-sign-identity", "bar")
).map(builder -> {
return builder.type(packageType);
}).mapMulti(ErrorTest::duplicateForMacSign).map(testCase -> {
if (packageType != PackageType.MAC_PKG) {
/*
* This is a bit tricky.
* The error output should also contain
*
* Error: Option [--mac-installer-sign-identity] is not valid with type [dmg]" error message.
*
* The order of errors is defined by the order of options on the command line causing them.
* If "--mac-installer-sign-identity" goes before "--mac-signing-key-user-name", the error output will be:
*
* Error: Option [--mac-installer-sign-identity] is not valid with type [dmg]
* Error: Mutually exclusive options [--mac-signing-key-user-name] and [--mac-installer-sign-identity]
*
* otherwise errors in the output will be in reverse order.
*/
var expectedMessages = new ArrayList<>(testCase.expectedMessages());
var invalidTypeOption = makeError("ERR_InvalidTypeOption", "--mac-installer-sign-identity", packageType.getType());
if (testCase.addArgs().indexOf("--mac-installer-sign-identity") < testCase.addArgs().indexOf("--mac-signing-key-user-name")) {
expectedMessages.addFirst(invalidTypeOption);
} else {
expectedMessages.add(invalidTypeOption);
}
testCase = testCase.copyWithExpectedMessages(expectedMessages);
}
return testCase;
}).toList());
}
return toTestArgs(testCases.stream());
}
public static Collection<Object[]> testLinux() {
final List<TestSpec> testCases = new ArrayList<>();
testCases.addAll(Stream.of(
testSpec().type(PackageType.LINUX_DEB).addArgs("--linux-package-name", "#")
.error("error.deb-invalid-value-for-package-name", "#")
.advice("error.deb-invalid-value-for-package-name.advice"),
testSpec().type(PackageType.LINUX_RPM).addArgs("--linux-package-name", "#")
.error("error.rpm-invalid-value-for-package-name", "#")
.advice("error.rpm-invalid-value-for-package-name.advice")
).map(TestSpec.Builder::create).toList());
invalidShortcut(testCases::add, "--linux-shortcut");
return toTestArgs(testCases.stream());
}
@Test(ifOS = MACOS)
@Parameter({"MAC_PKG", "--mac-signing-key-user-name", "false"})
@Parameter({"MAC_DMG", "--mac-signing-key-user-name", "false"})
@Parameter({"IMAGE", "--mac-signing-key-user-name", "false"})
@Parameter({"MAC_PKG", "--mac-app-image-sign-identity", "true"})
@Parameter({"MAC_DMG", "--mac-app-image-sign-identity", "true"})
@Parameter({"IMAGE", "--mac-app-image-sign-identity", "true"})
@Parameter({"MAC_PKG", "--mac-installer-sign-identity", "true"})
public static void testMacSigningIdentityValidation(PackageType type, String option, boolean passThroughOption) {
final var signingId = "foo";
final List<CannedFormattedString> errorMessages = new ArrayList<>();
errorMessages.add(makeError("error.cert.not.found", "Developer ID Application: " + signingId, ""));
final var cmd = JPackageCommand.helloAppImage()
.ignoreDefaultVerbose(true)
.addArguments("--mac-sign")
.addArguments(option, signingId)
.setPackageType(type);
if (passThroughOption) {
errorMessages.stream()
.map(CannedFormattedString::getValue)
.map(TKit::assertTextStream)
.map(TKit.TextStreamVerifier::negate).forEach(cmd::validateErr);
} else {
cmd.validateErr(errorMessages.toArray(CannedFormattedString[]::new));
}
cmd.execute(1);
}
private static void duplicate(TestSpec.Builder builder, Consumer<TestSpec> accumulator, Consumer<TestSpec.Builder> mutator) {
accumulator.accept(builder.create());
mutator.accept(builder);
accumulator.accept(builder.create());
}
private static void duplicateAddArgs(TestSpec.Builder builder, Consumer<TestSpec> accumulator, String...args) {
duplicate(builder, accumulator, b -> b.addArgs(args));
}
private static void duplicateForMacSign(TestSpec.Builder builder, Consumer<TestSpec> accumulator) {
duplicateAddArgs(builder, accumulator, "--mac-sign");
}
private static void invalidShortcut(Consumer<TestSpec> accumulator, String shortcutOption) {
Objects.requireNonNull(shortcutOption);
Stream.of("true", "false", "").map(value -> {
return testSpec().nativeType().addArgs(shortcutOption, value).error("error.parameter-not-launcher-shortcut-dir", value, shortcutOption).create();
}).forEach(accumulator);
}
private static void macInvalidRuntime(Consumer<TestSpec> accumulator) {
var runtimeWithBinDirErr = makeError(
"error.invalid-runtime-image-bin-dir", JPackageCommand.cannedArgument(cmd -> {
return Path.of(cmd.getArgumentValue("--runtime-image"));
}, Token.JAVA_HOME.token()));
var runtimeWithBinDirErrAdvice = makeAdvice(
"error.invalid-runtime-image-bin-dir.advice", "--mac-app-store");
Stream.of(
testSpec().nativeType().addArgs("--mac-app-store", "--runtime-image", Token.JAVA_HOME.token())
.messages(runtimeWithBinDirErr, runtimeWithBinDirErrAdvice)
).map(TestSpec.Builder::create).forEach(accumulator);
Stream.of(
Token.INVALID_MAC_RUNTIME_BUNDLE,
Token.EMPTY_DIR,
Token.INVALID_MAC_RUNTIME_IMAGE
).map(MissingRuntimeFileError::missingLibjli).forEach(mapper -> {
Stream.of(
testSpec(),
testSpec().nativeType(),
testSpec().nativeType().noAppDesc()
).map(mapper::applyTo).map(TestSpec.Builder::create).forEach(accumulator);
});
}
private record MissingRuntimeFileError(Token runtimeDir, String missingFile) {
MissingRuntimeFileError {
Objects.requireNonNull(runtimeDir);
Objects.requireNonNull(missingFile);
}
static MissingRuntimeFileError missingLibjli(Token runtimeDir) {
if (runtimeDir == Token.INVALID_MAC_RUNTIME_BUNDLE) {
return new MissingRuntimeFileError(runtimeDir, "Contents/Home/lib/**/libjli.dylib");
} else {
return new MissingRuntimeFileError(runtimeDir, "lib/**/libjli.dylib");
}
}
TestSpec.Builder applyTo(TestSpec.Builder builder) {
return builder.addArgs("--runtime-image", runtimeDir.token()).messages(expectedErrorMsg());
}
private CannedFormattedString expectedErrorMsg() {
return makeError(
"error.invalid-runtime-image-missing-file", JPackageCommand.cannedArgument(cmd -> {
return Path.of(cmd.getArgumentValue("--runtime-image"));
}, runtimeDir.token()), missingFile);
}
}
private record UnsupportedPlatformOption(String name, Optional<String> value) {
UnsupportedPlatformOption {
Objects.requireNonNull(name);
Objects.requireNonNull(value);
}
UnsupportedPlatformOption(String name) {
this(name, Optional.empty());
}
UnsupportedPlatformOption(String name, String value) {
this(name, Optional.of(value));
}
TestSpec toTestSpec() {
return value.map(v -> testSpec().unsupportedPlatformOption(name, v)).orElseGet(
() -> testSpec().unsupportedPlatformOption(name)).create();
}
static Collection<Object[]> createTestArgs(UnsupportedPlatformOption... options) {
return toTestArgs(Stream.of(options).map(UnsupportedPlatformOption::toTestSpec));
}
}
public static Collection<Object[]> winOption() {
return UnsupportedPlatformOption.createTestArgs(
new UnsupportedPlatformOption("--win-console"),
new UnsupportedPlatformOption("--win-dir-chooser"),
new UnsupportedPlatformOption("--win-help-url", "url"),
new UnsupportedPlatformOption("--win-menu"),
new UnsupportedPlatformOption("--win-menu-group", "name"),
new UnsupportedPlatformOption("--win-per-user-install"),
new UnsupportedPlatformOption("--win-shortcut"),
new UnsupportedPlatformOption("--win-shortcut-prompt"),
new UnsupportedPlatformOption("--win-update-url", "url"),
new UnsupportedPlatformOption("--win-upgrade-uuid", "uuid")
);
}
public static Collection<Object[]> linuxOption() {
return UnsupportedPlatformOption.createTestArgs(
new UnsupportedPlatformOption("--linux-package-name", "name"),
new UnsupportedPlatformOption("--linux-deb-maintainer", "email-address"),
new UnsupportedPlatformOption("--linux-menu-group", "menu-group-name"),
new UnsupportedPlatformOption("--linux-package-deps", "deps"),
new UnsupportedPlatformOption("--linux-rpm-license-type", "type"),
new UnsupportedPlatformOption("--linux-app-release", "release"),
new UnsupportedPlatformOption("--linux-app-category", "category-value"),
new UnsupportedPlatformOption("--linux-shortcut")
);
}
public static Collection<Object[]> macOption() {
return UnsupportedPlatformOption.createTestArgs(
new UnsupportedPlatformOption("--mac-package-identifier", "identifier"),
new UnsupportedPlatformOption("--mac-package-name", "name"),
new UnsupportedPlatformOption("--mac-package-signing-prefix", "prefix"),
new UnsupportedPlatformOption("--mac-sign"),
new UnsupportedPlatformOption("--mac-signing-keychain", "keychain-name"),
new UnsupportedPlatformOption("--mac-signing-key-user-name", "name"),
new UnsupportedPlatformOption("--mac-app-store"),
new UnsupportedPlatformOption("--mac-entitlements", "path"),
new UnsupportedPlatformOption("--mac-app-category", "category"),
new UnsupportedPlatformOption("--mac-dmg-content", "additional-content")
);
}
private static Collection<Object[]> toTestArgs(Stream<?> stream) {
return stream.map(v -> {
if (v instanceof TestSpec.Builder builder) {
return builder.create();
} else {
return v;
}
}).filter(v -> {
if (v instanceof TestSpec ts) {
return ts.isSupported();
} else {
return true;
}
}).map(v -> {
return new Object[] {v};
}).toList();
}
private static Collection<Object[]> toTestArgs(Collection<?> col) {
return toTestArgs(col.stream());
}
private static String adjustTextStreamVerifierArg(String str) {
return LINE_SEP_REGEXP.split(str)[0];
}
private static final Pattern LINE_SEP_REGEXP = Pattern.compile("\\R");
private final class SignEnvMock {
enum SingleCertificateKeychain {
FOO(CertificateType.CODE_SIGN),
BAR(CertificateType.INSTALLER),
;
SingleCertificateKeychain(CertificateType certificateType) {
this.keychain = KeychainWithCertsSpec.build()
.name(name().toLowerCase() + ".keychain")
.addCert(CertificateRequest.build()
.userName(name().toLowerCase())
.type(Objects.requireNonNull(certificateType)))
.create();
}
static List<KeychainWithCertsSpec> signingEnv() {
return Stream.of(values()).map(v -> {
return v.keychain;
}).toList();
}
CertificateType certificateType() {
return keychain.certificateRequests().getFirst().type();
}
ResolvedKeychain keychain() {
return new ResolvedKeychain(keychain).toMock(VALUE.env());
}
private final KeychainWithCertsSpec keychain;
}
static final MacSignMockUtils.SignEnv VALUE = new MacSignMockUtils.SignEnv(SingleCertificateKeychain.signingEnv());
}
}