1 /*
   2  * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.oracle.tools.packager.mac;
  27 
  28 import com.oracle.tools.packager.AbstractBundler;
  29 import com.oracle.tools.packager.Bundler;
  30 import com.oracle.tools.packager.BundlerParamInfo;
  31 import com.oracle.tools.packager.ConfigException;
  32 import com.oracle.tools.packager.IOUtils;
  33 import com.oracle.tools.packager.Log;
  34 import com.oracle.tools.packager.RelativeFileSet;
  35 import com.oracle.tools.packager.UnsupportedPlatformException;
  36 import org.junit.After;
  37 import org.junit.Assume;
  38 import org.junit.Before;
  39 import org.junit.BeforeClass;
  40 import org.junit.Ignore;
  41 import org.junit.Test;
  42 
  43 import java.io.File;
  44 import java.io.IOException;
  45 import java.nio.file.Files;
  46 import java.util.ArrayList;
  47 import java.util.Arrays;
  48 import java.util.Collection;
  49 import java.util.HashMap;
  50 import java.util.HashSet;
  51 import java.util.List;
  52 import java.util.Map;
  53 import java.util.Set;
  54 import java.util.TreeMap;
  55 
  56 import static com.oracle.tools.packager.StandardBundlerParam.*;
  57 import static com.oracle.tools.packager.mac.MacAppBundler.*;
  58 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.SIGNING_KEYCHAIN;
  59 import static com.oracle.tools.packager.mac.MacPkgBundler.DEVELOPER_ID_INSTALLER_SIGNING_KEY;
  60 import static org.junit.Assert.*;
  61 
  62 public class MacAppBundlerTest {
  63 
  64     static File tmpBase;
  65     static File workDir;
  66     static File appResourcesDir;
  67     static File fakeMainJar;
  68     static File hdpiIcon;
  69     static String runtimeJdk;
  70     static String runtimeJre;
  71     static Set<File> appResources;
  72     static boolean retain = false;
  73     static boolean signingKeysPresent = false;
  74 
  75     static final File FAKE_CERT_ROOT = new File("build/tmp/tests/cert/");
  76     
  77     @BeforeClass
  78     public static void prepareApp() {
  79         // only run on mac
  80         Assume.assumeTrue(System.getProperty("os.name").toLowerCase().contains("os x"));
  81 
  82         runtimeJdk = System.getenv("PACKAGER_JDK_ROOT");
  83         runtimeJre = System.getenv("PACKAGER_JRE_ROOT");
  84 
  85         // and only if we have the correct JRE settings
  86         String jre = System.getProperty("java.home").toLowerCase();
  87         Assume.assumeTrue(runtimeJdk != null || jre.endsWith("/contents/home/jre") || jre.endsWith("/contents/home/jre"));
  88 
  89         Log.setLogger(new Log.Logger(true));
  90         Log.setDebug(true);
  91 
  92         retain = Boolean.parseBoolean(System.getProperty("RETAIN_PACKAGER_TESTS"));
  93 
  94         workDir = new File("build/tmp/tests", "macapp");
  95         hdpiIcon = new File("build/tmp/tests", "GenericAppHiDPI.icns");
  96         appResourcesDir = new File("build/tmp/tests", "appResources");
  97         fakeMainJar = new File(appResourcesDir, "mainApp.jar");
  98 
  99         appResources = new HashSet<>(Arrays.asList(fakeMainJar));
 100 
 101         signingKeysPresent = DEVELOPER_ID_INSTALLER_SIGNING_KEY.fetchFrom(new TreeMap<>()) != null;
 102     }
 103 
 104     @Before
 105     public void createTmpDir() throws IOException {
 106         if (retain) {
 107             tmpBase = new File("build/tmp/tests/macapp");
 108         } else {
 109             tmpBase = BUILD_ROOT.fetchFrom(new TreeMap<>());
 110         }
 111         tmpBase.mkdir();
 112     }
 113 
 114 
 115     public String createFakeCerts(Map<String, ? super Object> p) {
 116         File config = new File(FAKE_CERT_ROOT, "app-cert.cfg");
 117         config.getParentFile().mkdirs();
 118         try {
 119             // create the config file holding the key config
 120             Files.write(config.toPath(), Arrays.<String>asList("[ codesign ]",
 121                     "keyUsage=critical,digitalSignature",
 122                     "basicConstraints=critical,CA:false",
 123                     "extendedKeyUsage=critical,codeSigning"));
 124 
 125             // create the SSL keys
 126             ProcessBuilder pb = new ProcessBuilder("openssl", "req",
 127                     "-newkey", "rsa:2048",
 128                     "-nodes",
 129                     "-out", FAKE_CERT_ROOT + "/app.csr",
 130                     "-keyout", FAKE_CERT_ROOT + "/app.key",
 131                     "-subj", "/CN=Developer ID Application: Insecure Test Cert/OU=JavaFX Dev/O=Oracle/C=US");
 132             IOUtils.exec(pb, VERBOSE.fetchFrom(p));
 133 
 134             // create the cert
 135             pb = new ProcessBuilder("openssl", "x509",
 136                     "-req",
 137                     "-days", "1",
 138                     "-in", FAKE_CERT_ROOT + "/app.csr",
 139                     "-signkey", FAKE_CERT_ROOT + "/app.key",
 140                     "-out", FAKE_CERT_ROOT + "/app.crt",
 141                     "-extfile", FAKE_CERT_ROOT + "/app.cnf",
 142                     "-extensions", "codesign");
 143             IOUtils.exec(pb, VERBOSE.fetchFrom(p));
 144 
 145             // create and add it to the keychain
 146             pb = new ProcessBuilder("certtool",
 147                     "i", FAKE_CERT_ROOT + "/app.crt",
 148                     "k=" + FAKE_CERT_ROOT + "/app.keychain",
 149                     "r=" + FAKE_CERT_ROOT + "/app.key",
 150                     "c",
 151                     "p=");
 152             IOUtils.exec(pb, VERBOSE.fetchFrom(p));
 153 
 154             return FAKE_CERT_ROOT + "/app.keychain";
 155         } catch (IOException e) {
 156             e.printStackTrace();
 157         }
 158 
 159         return null;
 160     }
 161 
 162     @After
 163     public void maybeCleanupTmpDir() {
 164         if (!retain) {
 165             attemptDelete(tmpBase);
 166         }
 167         attemptDelete(FAKE_CERT_ROOT);
 168     }
 169 
 170     private void attemptDelete(File tmpBase) {
 171         if (tmpBase.isDirectory()) {
 172             File[] children = tmpBase.listFiles();
 173             if (children != null) {
 174                 for (File f : children) {
 175                     attemptDelete(f);
 176                 }
 177             }
 178         }
 179         boolean success;
 180         try {
 181             success = !tmpBase.exists() || tmpBase.delete();
 182         } catch (SecurityException se) {
 183             success = false;
 184         }
 185         if (!success) {
 186             System.err.println("Could not clean up " + tmpBase.toString());
 187         }
 188     }
 189 
 190 
 191     @Test
 192     public void testValidateVersion() {
 193         MacAppBundler b = new MacAppBundler();
 194         String validVersions[] = {"1", "255", "1.0", "1.0.0", "255.255.0", "255.255.6000"};
 195         String invalidVersions[] = {null, "alpha", "1.0-alpha", "0.300", "-300", "1.-1", "1.1.-1"};
 196 
 197         for(String v: validVersions) {
 198             assertTrue("Expect to be valid ["+v+"]",
 199                     MacAppBundler.validCFBundleVersion(v));
 200             try {
 201                 Map<String, Object> params = new HashMap<>();
 202                 params.put(BUILD_ROOT.getID(), tmpBase);
 203                 params.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 204 
 205                 if (runtimeJdk != null) {
 206                     params.put(MAC_RUNTIME.getID(), runtimeJdk);
 207                 }
 208 
 209                 params.put(VERSION.getID(), v);
 210                 b.validate(params);
 211             } catch (ConfigException ce) {
 212                 ce.printStackTrace();
 213                 assertTrue("Expect to be valid via '" + VERSION.getID() + "' ["+v+"]",
 214                         false);
 215             } catch (UnsupportedPlatformException ignore) {
 216             }
 217             try {
 218                 Map<String, Object> params = new HashMap<>();
 219                 params.put(BUILD_ROOT.getID(), tmpBase);
 220                 params.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 221 
 222                 if (runtimeJdk != null) {
 223                     params.put(MAC_RUNTIME.getID(), runtimeJdk);
 224                 }
 225 
 226                 params.put(MAC_CF_BUNDLE_VERSION.getID(), v);
 227                 b.validate(params);
 228             } catch (ConfigException ce) {
 229                 assertTrue("Expect to be valid via '" + VERSION.getID() + "' ["+v+"]",
 230                         false);
 231             } catch (UnsupportedPlatformException ignore) {
 232             }
 233         }
 234 
 235         for(String v: invalidVersions) {
 236             assertFalse("Expect to be invalid ["+v+"]",
 237                     MacAppBundler.validCFBundleVersion(v));
 238             try {
 239                 Map<String, Object> params = new HashMap<>();
 240                 params.put(BUILD_ROOT.getID(), tmpBase);
 241                 params.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 242 
 243                 if (runtimeJdk != null) {
 244                     params.put(MAC_RUNTIME.getID(), runtimeJdk);
 245                 }
 246 
 247                 params.put(VERSION.getID(), v);
 248                 b.validate(params);
 249                 assertFalse("Invalid appVersion is not the mac.CFBundleVersion", MAC_CF_BUNDLE_VERSION.fetchFrom(params).equals(VERSION.fetchFrom(params)));
 250             } catch (ConfigException ce) {
 251                 ce.printStackTrace();
 252                 assertTrue("Expect to be ignored when invalid via '" + VERSION.getID() + "' ["+v+"]",
 253                         false);
 254             } catch (UnsupportedPlatformException ignore) {
 255             }
 256             try {
 257                 Map<String, Object> params = new HashMap<>();
 258                 params.put(BUILD_ROOT.getID(), tmpBase);
 259                 params.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 260 
 261                 if (runtimeJdk != null) {
 262                     params.put(MAC_RUNTIME.getID(), runtimeJdk);
 263                 }
 264 
 265                 params.put(MAC_CF_BUNDLE_VERSION.getID(), v);
 266                 b.validate(params);
 267                 assertTrue("Expect to be invalid via '" + VERSION.getID() + "' ["+v+"]",
 268                         false);
 269             } catch (ConfigException | UnsupportedPlatformException ignore) {
 270             }
 271         }
 272     }
 273 
 274 
 275     /**
 276      * See if smoke comes out
 277      */
 278     @Test
 279     public void smokeTest() throws IOException, ConfigException, UnsupportedPlatformException {
 280         AbstractBundler bundler = new MacAppBundler();
 281 
 282         assertNotNull(bundler.getName());
 283         assertNotNull(bundler.getID());
 284         assertNotNull(bundler.getDescription());
 285         //assertNotNull(bundler.getBundleParameters());
 286 
 287         Map<String, Object> bundleParams = new HashMap<>();
 288 
 289         bundleParams.put(BUILD_ROOT.getID(), tmpBase);
 290 
 291         bundleParams.put(APP_NAME.getID(), "Smoke Test App");
 292         bundleParams.put(MAC_CF_BUNDLE_NAME.getID(), "Smoke");
 293         bundleParams.put(MAIN_CLASS.getID(), "hello.HelloRectangle");
 294         bundleParams.put(PREFERENCES_ID.getID(), "the/really/long/preferences/id");
 295         bundleParams.put(MAIN_JAR.getID(),
 296                 new RelativeFileSet(fakeMainJar.getParentFile(),
 297                         new HashSet<>(Arrays.asList(fakeMainJar)))
 298         );
 299         bundleParams.put(CLASSPATH.getID(), "mainApp.jar");
 300         bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 301         bundleParams.put(VERBOSE.getID(), true);
 302         bundleParams.put(SIGN_BUNDLE.getID(), false); 
 303 
 304         if (runtimeJdk != null) {
 305             bundleParams.put(MAC_RUNTIME.getID(), runtimeJdk);
 306         }
 307 
 308         boolean valid = bundler.validate(bundleParams);
 309         assertTrue(valid);
 310 
 311         File result = bundler.execute(bundleParams, new File(workDir, "smoke"));
 312         System.err.println("Bundle at - " + result);
 313         assertNotNull(result);
 314         assertTrue(result.exists());
 315     }
 316 
 317     /**
 318      * Set File Association
 319      */
 320     @Test
 321     public void testFileAssociation()
 322         throws IOException, ConfigException, UnsupportedPlatformException
 323     {
 324         // only run the bundle with full tests
 325         Assume.assumeTrue(Boolean.parseBoolean(System.getProperty("FULL_TEST")));
 326 
 327         testFileAssociation("FASmoke 1", "Bogus File", "bogus", "application/x-vnd.test-bogus",
 328                             new File(appResourcesDir, "test.icns"));        
 329     }
 330     
 331     @Test
 332     public void testFileAssociationWithNullExtension()
 333         throws IOException, ConfigException, UnsupportedPlatformException
 334     {
 335         // association with no extension is still valid case (see RT-38625)
 336         testFileAssociation("FASmoke null", "Bogus File", null, "application/x-vnd.test-bogus",
 337                             new File(appResourcesDir, "test.icns"));
 338     }
 339 
 340     @Test
 341     public void testFileAssociationWithMultipleExtension()
 342             throws IOException, ConfigException, UnsupportedPlatformException
 343     {
 344         // only run the bundle with full tests
 345         Assume.assumeTrue(Boolean.parseBoolean(System.getProperty("FULL_TEST")));
 346 
 347         testFileAssociation("FASmoke ME", "Bogus File", "bogus fake", "application/x-vnd.test-bogus",
 348                 new File(appResourcesDir, "test.icns"));
 349     }
 350 
 351     @Test
 352     public void testMultipleFileAssociation()
 353             throws IOException, ConfigException, UnsupportedPlatformException
 354     {
 355         // only run the bundle with full tests
 356         Assume.assumeTrue(Boolean.parseBoolean(System.getProperty("FULL_TEST")));
 357 
 358         testFileAssociationMultiples("FASmoke MA",
 359                 new String[]{"Bogus File", "Fake file"},
 360                 new String[]{"bogus", "fake"},
 361                 new String[]{"application/x-vnd.test-bogus", "application/x-vnd.test-fake"},
 362                 new File[]{new File(appResourcesDir, "test.icns"), new File(appResourcesDir, "test.icns")});
 363     }
 364 
 365     @Test
 366     public void testMultipleFileAssociationWithMultipleExtension()
 367             throws IOException, ConfigException, UnsupportedPlatformException
 368     {
 369         // association with no extension is still valid case (see RT-38625)
 370         testFileAssociationMultiples("FASmoke MAME",
 371                 new String[]{"Bogus File", "Fake file"},
 372                 new String[]{"bogus boguser", "fake faker"},
 373                 new String[]{"application/x-vnd.test-bogus", "application/x-vnd.test-fake"},
 374                 new File[]{new File(appResourcesDir, "test.icns"), new File(appResourcesDir, "test.icns")});
 375     }
 376 
 377     private void testFileAssociation(String appName, String description, String extensions,
 378                                      String contentType, File icon)
 379             throws IOException, ConfigException, UnsupportedPlatformException
 380     {
 381         testFileAssociationMultiples(appName, new String[] {description}, new String[] {extensions},
 382                 new String[] {contentType}, new File[] {icon});
 383     }
 384 
 385     private void testFileAssociationMultiples(String appName, String[] description, String[] extensions,
 386                                               String[] contentType, File[] icon)
 387             throws IOException, ConfigException, UnsupportedPlatformException
 388     {
 389         assertEquals("Sanity: description same length as extensions", description.length, extensions.length);
 390         assertEquals("Sanity: extensions same length as contentType", extensions.length, contentType.length);
 391         assertEquals("Sanity: contentType same length as icon", contentType.length, icon.length);
 392 
 393         AbstractBundler bundler = new MacAppBundler();
 394 
 395         assertNotNull(bundler.getName());
 396         assertNotNull(bundler.getID());
 397         assertNotNull(bundler.getDescription());
 398         //assertNotNull(bundler.getBundleParameters());
 399 
 400         Map<String, Object> bundleParams = new HashMap<>();
 401 
 402         bundleParams.put(BUILD_ROOT.getID(), tmpBase);
 403 
 404         if (runtimeJdk != null) {
 405             bundleParams.put(MAC_RUNTIME.getID(), runtimeJdk);
 406         }
 407 
 408         bundleParams.put(APP_NAME.getID(), appName);
 409         bundleParams.put(MAIN_CLASS.getID(), "hello.HelloRectangle");
 410         bundleParams.put(MAIN_JAR.getID(),
 411                 new RelativeFileSet(fakeMainJar.getParentFile(),
 412                         new HashSet<>(Arrays.asList(fakeMainJar)))
 413         );
 414         bundleParams.put(CLASSPATH.getID(), "mainApp.jar");
 415         bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 416         bundleParams.put(VERBOSE.getID(), true);
 417         bundleParams.put(SIGN_BUNDLE.getID(), false);
 418 
 419         List<Map<String, Object>> associations = new ArrayList<>();
 420 
 421         for (int i = 0; i < description.length; i++) {
 422             Map<String, Object> fileAssociation = new HashMap<>();
 423             fileAssociation.put(FA_DESCRIPTION.getID(), description[i]);
 424             fileAssociation.put(FA_EXTENSIONS.getID(), extensions[i]);
 425             fileAssociation.put(FA_CONTENT_TYPE.getID(), contentType[i]);
 426             fileAssociation.put(FA_ICON.getID(), icon[i]);
 427 
 428             associations.add(fileAssociation);
 429         }
 430 
 431         bundleParams.put(FILE_ASSOCIATIONS.getID(), associations);
 432 
 433         boolean valid = bundler.validate(bundleParams);
 434         assertTrue(valid);
 435 
 436         File result = bundler.execute(bundleParams, new File(workDir, APP_FS_NAME.fetchFrom(bundleParams)));
 437         System.err.println("Bundle at - " + result);
 438         assertNotNull(result);
 439         assertTrue(result.exists());
 440     }
 441 
 442     /**
 443      * Build signed smoke test and mark it as quarantined, skip if no keys present
 444      */
 445     @Test
 446     public void quarantinedAppTest() throws IOException, ConfigException, UnsupportedPlatformException {
 447         AbstractBundler bundler = new MacAppBundler();
 448 
 449         assertNotNull(bundler.getName());
 450         assertNotNull(bundler.getID());
 451         assertNotNull(bundler.getDescription());
 452         //assertNotNull(bundler.getBundleParameters());
 453 
 454         Map<String, Object> bundleParams = new HashMap<>();
 455 
 456         bundleParams.put(BUILD_ROOT.getID(), tmpBase);
 457 
 458         bundleParams.put(APP_NAME.getID(), "Quarantined Test App");
 459         bundleParams.put(MAC_CF_BUNDLE_NAME.getID(), "Quarantine");
 460         bundleParams.put(MAIN_CLASS.getID(), "hello.HelloRectangle");
 461         bundleParams.put(PREFERENCES_ID.getID(), "the/really/long/preferences/id");
 462         bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 463         bundleParams.put(VERBOSE.getID(), true);
 464 
 465         if (runtimeJdk != null) {
 466             bundleParams.put(MAC_RUNTIME.getID(), runtimeJdk);
 467         }
 468 
 469         if (!signingKeysPresent) {
 470             String keychain = createFakeCerts(bundleParams);
 471             Assume.assumeNotNull(keychain);
 472             bundleParams.put(SIGNING_KEYCHAIN.getID(), keychain);
 473         }
 474 
 475         boolean valid = bundler.validate(bundleParams);
 476         assertTrue(valid);
 477 
 478         File result = bundler.execute(bundleParams, new File(workDir, "quarantine"));
 479         System.err.println("Bundle at - " + result);
 480         assertNotNull(result);
 481         assertTrue(result.exists());
 482         validateSignatures(result);
 483 
 484         // mark it as though it's been downloaded
 485         ProcessBuilder pb = new ProcessBuilder(
 486                 "xattr", "-w", "com.apple.quarantine",
 487                 "0000;" + Long.toHexString(System.currentTimeMillis() / 1000L) + ";Java Unit Tests;|com.oracle.jvm.8u",
 488                 result.toString());
 489         IOUtils.exec(pb, true);
 490     }
 491 
 492     /**
 493      * The bare minimum configuration needed to make it work
 494      * <ul>
 495      *     <li>Where to build it</li>
 496      *     <li>The jar containing the application (with a main-class attribute)</li>
 497      * </ul>
 498      *
 499      * All other values will be driven off of those two values.
 500      */
 501     @Test
 502     public void minimumConfig() throws IOException, ConfigException, UnsupportedPlatformException {
 503         Bundler bundler = new MacAppBundler();
 504 
 505         Map<String, Object> bundleParams = new HashMap<>();
 506 
 507         // not part of the typical setup, for testing
 508         bundleParams.put(BUILD_ROOT.getID(), tmpBase);
 509 
 510         bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 511 
 512         if (runtimeJdk != null) {
 513             bundleParams.put(MAC_RUNTIME.getID(), runtimeJdk);
 514         }
 515 
 516         String keychain = null;
 517         if (!signingKeysPresent) {
 518             keychain = createFakeCerts(bundleParams);
 519             if (keychain != null) {
 520                 bundleParams.put(SIGNING_KEYCHAIN.getID(), keychain);
 521             }
 522         }
 523 
 524         File output = bundler.execute(bundleParams, new File(workDir, "BareMinimum"));
 525         System.err.println("Bundle at - " + output);
 526         assertNotNull(output);
 527         assertTrue(output.exists());
 528         if (signingKeysPresent || keychain != null) {
 529             validateSignatures(output);
 530         }
 531     }
 532 
 533     /**
 534      * Test with unicode in places we expect it to be
 535      */
 536     @Test
 537     public void unicodeConfig() throws IOException, ConfigException, UnsupportedPlatformException {
 538         Bundler bundler = new MacAppBundler();
 539 
 540         Map<String, Object> bundleParams = new HashMap<>();
 541 
 542         bundleParams.put(BUILD_ROOT.getID(), tmpBase);
 543 
 544         bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 545 
 546         bundleParams.put(APP_NAME.getID(), "хелловорлд");
 547         bundleParams.put(TITLE.getID(), "ХеллоВорлд аппликейшн");
 548         bundleParams.put(VENDOR.getID(), "Оракл девелопмент");
 549         bundleParams.put(DESCRIPTION.getID(), "крайне большое описание со странными символами");
 550 
 551         if (runtimeJdk != null) {
 552             bundleParams.put(MAC_RUNTIME.getID(), runtimeJdk);
 553         }
 554 
 555         String keychain = null;
 556         if (!signingKeysPresent) {
 557             keychain = createFakeCerts(bundleParams);
 558             if (keychain != null) {
 559                 bundleParams.put(SIGNING_KEYCHAIN.getID(), keychain);
 560             }
 561         }
 562 
 563         bundler.validate(bundleParams);
 564 
 565         File output = bundler.execute(bundleParams, new File(workDir, "Unicode"));
 566         System.err.println("Bundle at - " + output);
 567         assertNotNull(output);
 568         assertTrue(output.exists());
 569         if (signingKeysPresent || keychain != null) {
 570             validateSignatures(output);
 571         }
 572     }
 573 
 574     /**
 575      * Test a misconfiguration where the runtime is misconfigured.
 576      */
 577     @Test(expected = ConfigException.class)
 578     public void runtimeBad() throws IOException, ConfigException, UnsupportedPlatformException {
 579         Bundler bundler = new MacAppBundler();
 580 
 581         Map<String, Object> bundleParams = new HashMap<>();
 582 
 583         bundleParams.put(BUILD_ROOT.getID(), tmpBase);
 584         bundleParams.put(VERBOSE.getID(), true);
 585 
 586         bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 587         bundleParams.put(MAC_RUNTIME.getID(), APP_RESOURCES.fetchFrom(bundleParams));
 588 
 589         bundler.validate(bundleParams);
 590     }
 591 
 592     /**
 593      * Test a misconfiguration where signature is requested but no key is kept.
 594      */
 595     @Test(expected = ConfigException.class)
 596     public void signButNoCert() throws IOException, ConfigException, UnsupportedPlatformException {
 597         Bundler bundler = new MacAppBundler();
 598 
 599         Map<String, Object> bundleParams = new HashMap<>();
 600 
 601         bundleParams.put(BUILD_ROOT.getID(), tmpBase);
 602         bundleParams.put(VERBOSE.getID(), true);
 603 
 604         bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 605 
 606         bundleParams.put(SIGN_BUNDLE.getID(), true);
 607         bundleParams.put(DEVELOPER_ID_APP_SIGNING_KEY.getID(), null);
 608 
 609         bundler.validate(bundleParams);
 610     }
 611 
 612     @Test
 613     public void configureEverything() throws Exception {
 614         AbstractBundler bundler = new MacAppBundler();
 615         Collection<BundlerParamInfo<?>> parameters = bundler.getBundleParameters();
 616 
 617         Map<String, Object> bundleParams = new HashMap<>();
 618 
 619         bundleParams.put(APP_NAME.getID(), "Everything App Name");
 620         bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 621         bundleParams.put(ARGUMENTS.getID(), Arrays.asList("He Said", "She Said"));
 622         bundleParams.put(BUNDLE_ID_SIGNING_PREFIX.getID(), "everything.signing.prefix.");
 623         bundleParams.put(CLASSPATH.getID(), "mainApp.jar");
 624         bundleParams.put(DEVELOPER_ID_APP_SIGNING_KEY.getID(), "Developer ID Application");
 625         bundleParams.put(ICON_ICNS.getID(), hdpiIcon);
 626         bundleParams.put(JVM_OPTIONS.getID(), "-Xms128M");
 627         bundleParams.put(JVM_PROPERTIES.getID(), "everything.jvm.property=everything.jvm.property.value");
 628         bundleParams.put(MAC_CATEGORY.getID(), "public.app-category.developer-tools");
 629         bundleParams.put(MAC_CF_BUNDLE_IDENTIFIER.getID(), "com.example.everything.cf-bundle-identifier");
 630         bundleParams.put(MAC_CF_BUNDLE_NAME.getID(), "Everything CF Bundle Name");
 631         bundleParams.put(MAC_CF_BUNDLE_VERSION.getID(), "8.2.0");
 632         bundleParams.put(MAC_RUNTIME.getID(), runtimeJdk == null ? System.getProperty("java.home") : runtimeJdk);
 633         bundleParams.put(MAIN_CLASS.getID(), "hello.HelloRectangle");
 634         bundleParams.put(MAIN_JAR.getID(), "mainApp.jar");
 635         bundleParams.put(PREFERENCES_ID.getID(), "everything/preferences/id");
 636         bundleParams.put(PRELOADER_CLASS.getID(), "hello.HelloPreloader");
 637         bundleParams.put(SIGNING_KEYCHAIN.getID(), "");
 638         bundleParams.put(USER_JVM_OPTIONS.getID(), "-Xmx=256M\n");
 639         bundleParams.put(VERSION.getID(), "1.2.3.4");
 640 
 641         // assert they are set
 642         for (BundlerParamInfo bi :parameters) {
 643             assertTrue("Bundle args should contain " + bi.getID(), bundleParams.containsKey(bi.getID()));
 644         }
 645 
 646         // and only those are set
 647         bundleParamLoop:
 648         for (String s :bundleParams.keySet()) {
 649             for (BundlerParamInfo<?> bpi : parameters) {
 650                 if (s.equals(bpi.getID())) {
 651                     continue bundleParamLoop;
 652                 }
 653             }
 654             fail("Enumerated parameters does not contain " + s);
 655         }
 656 
 657         // assert they resolve
 658         for (BundlerParamInfo bi :parameters) {
 659             bi.fetchFrom(bundleParams);
 660         }
 661 
 662         // add verbose now that we are done scoping out parameters
 663         bundleParams.put(BUILD_ROOT.getID(), tmpBase);
 664         bundleParams.put(VERBOSE.getID(), true);
 665         bundleParams.put(SIGN_BUNDLE.getID(), false);
 666 
 667         // assert it validates
 668         boolean valid = bundler.validate(bundleParams);
 669         assertTrue(valid);
 670 
 671         // only run the bundle with full tests
 672         Assume.assumeTrue(Boolean.parseBoolean(System.getProperty("FULL_TEST")));
 673 
 674         // but first remove signing keys, test servers don't have these...
 675         bundleParams.remove(DEVELOPER_ID_APP_SIGNING_KEY.getID());
 676 
 677         File result = bundler.execute(bundleParams, new File(workDir, "everything"));
 678         System.err.println("Bundle at - " + result);
 679         assertNotNull(result);
 680         assertTrue(result.exists());
 681     }
 682 
 683     @Ignore // this test is noisy and only valid for by-hand validation
 684     @Test
 685     public void jvmUserOptionsTest() throws IOException, ConfigException, UnsupportedPlatformException {
 686 
 687         for (String name : Arrays.asList("", "example", "com.example", "com.example.helloworld", "com.example.hello.world", "com.example.hello.world.app")) {
 688 
 689             AbstractBundler bundler = new MacAppBundler();
 690 
 691             Map<String, Object> bundleParams = new HashMap<>();
 692 
 693             bundleParams.put(BUILD_ROOT.getID(), tmpBase);
 694 
 695             bundleParams.put(APP_NAME.getID(), "User JVM Options App - " + name);
 696             bundleParams.put(MAC_CF_BUNDLE_NAME.getID(), name + ".application");
 697             bundleParams.put(MAIN_CLASS.getID(), "hello.HelloRectangle");
 698             bundleParams.put(IDENTIFIER.getID(), name);
 699             bundleParams.put(PREFERENCES_ID.getID(), name.replace(".", "/"));
 700             bundleParams.put(MAIN_JAR.getID(),
 701                     new RelativeFileSet(fakeMainJar.getParentFile(),
 702                             new HashSet<>(Arrays.asList(fakeMainJar)))
 703             );
 704             bundleParams.put(CLASSPATH.getID(), "mainApp.jar");
 705             bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 706             bundleParams.put(VERBOSE.getID(), true);
 707             bundleParams.put(SIGN_BUNDLE.getID(), false); // force no signing
 708 
 709             if (runtimeJdk != null) {
 710                 bundleParams.put(MAC_RUNTIME.getID(), runtimeJdk);
 711             }
 712 
 713             boolean valid = bundler.validate(bundleParams);
 714             assertTrue(valid);
 715 
 716             File result = bundler.execute(bundleParams, new File(workDir, "UserOpts-" + name.replace(".", "-")));
 717             System.err.println("Bundle at - " + result);
 718             assertNotNull(result);
 719             assertTrue(result.exists());
 720         }
 721     }
 722 
 723 
 724     /**
 725      * User a JRE instead of a JDK
 726      */
 727     @Test
 728     public void testJRE() throws IOException, ConfigException, UnsupportedPlatformException {
 729         String jre = runtimeJre == null ? "/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/" : runtimeJre;
 730         Assume.assumeTrue(new File(jre).isDirectory());
 731 
 732         Bundler bundler = new MacAppBundler();
 733 
 734         Map<String, Object> bundleParams = new HashMap<>();
 735 
 736         // not part of the typical setup, for testing
 737         bundleParams.put(BUILD_ROOT.getID(), tmpBase);
 738 
 739         bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 740         bundleParams.put(MAC_RUNTIME.getID(), jre);
 741 
 742         String keychain = null;
 743         if (!signingKeysPresent) {
 744             keychain = createFakeCerts(bundleParams);
 745             if (keychain != null) {
 746                 bundleParams.put(SIGNING_KEYCHAIN.getID(), keychain);
 747             }
 748         }
 749 
 750         boolean valid = bundler.validate(bundleParams);
 751         assertTrue(valid);
 752 
 753         File output = bundler.execute(bundleParams, new File(workDir, "JRETest"));
 754         System.err.println("Bundle at - " + output);
 755         assertNotNull(output);
 756         assertTrue(output.exists());
 757         if (signingKeysPresent || keychain != null) {
 758             validateSignatures(output);
 759         }
 760     }
 761 
 762     /**
 763      * Verify a match on too many keys doesn't blow things up
 764      */
 765     @Test
 766     public void testTooManyKeyMatches() {
 767         Assume.assumeTrue(MacBaseInstallerBundler.findKey("Developer ID Application:", null, true) != null);
 768         Assume.assumeTrue(MacBaseInstallerBundler.findKey("Developer ID Installer:", null, true) != null);
 769         assertTrue(MacBaseInstallerBundler.findKey("Developer", null, true) == null);
 770         assertTrue(MacBaseInstallerBundler.findKey("A completely bogus key that should never realistically exist unless we are attempting to falsely break the tests", null, true) == null);
 771     }
 772     
 773     public void validateSignatures(File appLocation) throws IOException {
 774         // shallow validation
 775         ProcessBuilder pb = new ProcessBuilder(
 776                 "codesign", "--verify",
 777                 "-v", // single verbose
 778                 appLocation.getCanonicalPath());
 779         IOUtils.exec(pb, true);
 780 
 781         // deep validation
 782         pb = new ProcessBuilder(
 783                 "codesign", "--verify",
 784                 "--deep",
 785                 "-v", // single verbose
 786                 appLocation.getCanonicalPath());
 787         IOUtils.exec(pb, true);
 788         
 789         // only run spctl for pre-existing keys
 790         if (signingKeysPresent) {
 791             //spctl, this verifies gatekeeper
 792             pb = new ProcessBuilder(
 793                     "spctl", "--assess",
 794                     "-v", // single verbose
 795                     appLocation.getCanonicalPath());
 796             IOUtils.exec(pb, true);
 797         }
 798     }    
 799 
 800 
 801 }