diff --git a/lib/private/Installer.php b/lib/private/Installer.php
index 120992d3e632cc3620400576db79fe5453e23146..2366b762654edac4a41919d6cf2046b0ce5e81d1 100644
--- a/lib/private/Installer.php
+++ b/lib/private/Installer.php
@@ -209,10 +209,7 @@ class Installer {
 						sprintf(
 							'App with id %s has a certificate not issued by a trusted Code Signing Authority',
 							$appId
-						),
-						[
-							'app' => 'core',
-						]
+						)
 					);
 				}
 
@@ -223,10 +220,7 @@ class Installer {
 						sprintf(
 							'App with id %s has a cert with no CN',
 							$appId
-						),
-						[
-							'app' => 'core',
-						]
+						)
 					);
 				}
 				if($certInfo['subject']['CN'] !== $appId) {
@@ -235,10 +229,7 @@ class Installer {
 							'App with id %s has a cert issued to %s',
 							$appId,
 							$certInfo['subject']['CN']
-						),
-						[
-							'app' => 'core',
-						]
+						)
 					);
 				}
 
@@ -259,10 +250,22 @@ class Installer {
 
 					if($archive) {
 						$archive->extract($extractDir);
+						$allFiles = scandir($extractDir);
+						$folders = array_diff($allFiles, ['.', '..']);
+						$folders = array_values($folders);
+
+						if(count($folders) > 1) {
+							throw new \Exception(
+								sprintf(
+									'Extracted app %s has more than 1 folder',
+									$appId
+								)
+							);
+						}
 
 						// Check if appinfo/info.xml has the same app ID as well
 						$loadEntities = libxml_disable_entity_loader(false);
-						$xml = simplexml_load_file($extractDir . '/' . $appId . '/appinfo/info.xml');
+						$xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
 						libxml_disable_entity_loader($loadEntities);
 						if((string)$xml->id !== $appId) {
 							throw new \Exception(
@@ -270,10 +273,7 @@ class Installer {
 									'App for id %s has a wrong app ID in info.xml: %s',
 									$appId,
 									(string)$xml->id
-								),
-								[
-									'app' => 'core',
-								]
+								)
 							);
 						}
 
@@ -282,21 +282,19 @@ class Installer {
 						OC_Helper::rmdirr($baseDir);
 						// Move to app folder
 						if(@mkdir($baseDir)) {
-							$extractDir .= '/' . $appId;
+							$extractDir .= '/' . $folders[0];
 							OC_Helper::copyr($extractDir, $baseDir);
 						}
 						OC_Helper::copyr($extractDir, $baseDir);
 						OC_Helper::rmdirr($extractDir);
+						return;
 					} else {
 						throw new \Exception(
 							sprintf(
 								'Could not extract app with ID %s to %s',
 								$appId,
 								$extractDir
-							),
-							[
-								'app' => 'core',
-							]
+							)
 						);
 					}
 				} else {
@@ -305,14 +303,18 @@ class Installer {
 						sprintf(
 							'App with id %s has invalid signature',
 							$appId
-						),
-						[
-							'app' => 'core',
-						]
+						)
 					);
 				}
 			}
 		}
+
+		throw new \Exception(
+			sprintf(
+				'Could not download app %s',
+				$appId
+			)
+		);
 	}
 
 	/**
diff --git a/tests/data/testapp.tar.gz b/tests/data/testapp.tar.gz
new file mode 100644
index 0000000000000000000000000000000000000000..b9184c8c378ccb7b3b170b337db4581078811b0c
Binary files /dev/null and b/tests/data/testapp.tar.gz differ
diff --git a/tests/data/testapp1.tar.gz b/tests/data/testapp1.tar.gz
new file mode 100644
index 0000000000000000000000000000000000000000..f864edb2a6d7fd64b9522ba7520deb97354d1477
Binary files /dev/null and b/tests/data/testapp1.tar.gz differ
diff --git a/tests/lib/App/AppStore/Version/VersionParserTest.php b/tests/lib/App/AppStore/Version/VersionParserTest.php
index 77c289e547405e839450b241385366f0e9b0c9a6..ebfa98ade3918e5ebc1f1ff5598e17fdb0a46afb 100644
--- a/tests/lib/App/AppStore/Version/VersionParserTest.php
+++ b/tests/lib/App/AppStore/Version/VersionParserTest.php
@@ -88,4 +88,12 @@ class VersionParserTest extends TestCase  {
 	public function testGetVersionException() {
 		$this->versionParser->getVersion('BogusVersion');
 	}
+
+	/**
+	 * @expectedException \Exception
+	 * @expectedExceptionMessage Version cannot be parsed: >=8.2 <=9.1a
+	 */
+	public function testGetVersionExceptionWithMultiple() {
+		$this->versionParser->getVersion('>=8.2 <=9.1a');
+	}
 }
diff --git a/tests/lib/InstallerTest.php b/tests/lib/InstallerTest.php
index fb19ee94aa3c5b38e43fc7ebbd80bfa6ec2ced7d..1212d3d75592bb7ffcc84c8bd6dc19b209db1f0c 100644
--- a/tests/lib/InstallerTest.php
+++ b/tests/lib/InstallerTest.php
@@ -9,17 +9,44 @@
 namespace Test;
 
 
+use OC\App\AppStore\Fetcher\AppFetcher;
 use OC\Archive\ZIP;
 use OC\Installer;
+use OCP\Http\Client\IClient;
+use OCP\Http\Client\IClientService;
+use OCP\ILogger;
+use OCP\ITempManager;
 
 class InstallerTest extends TestCase {
 
 	private static $appid = 'testapp';
 	private $appstore;
+	/** @var AppFetcher|\PHPUnit_Framework_MockObject_MockObject */
+	private $appFetcher;
+	/** @var IClientService|\PHPUnit_Framework_MockObject_MockObject */
+	private $clientService;
+	/** @var ITempManager|\PHPUnit_Framework_MockObject_MockObject */
+	private $tempManager;
+	/** @var ILogger|\PHPUnit_Framework_MockObject_MockObject */
+	private $logger;
+
+	/** @var Installer */
+	private $installer;
 
 	protected function setUp() {
 		parent::setUp();
 
+		$this->appFetcher = $this->createMock(AppFetcher::class);
+		$this->clientService = $this->createMock(IClientService::class);
+		$this->tempManager = $this->createMock(ITempManager::class);
+		$this->logger = $this->createMock(ILogger::class);
+		$this->installer = new Installer(
+			$this->appFetcher,
+			$this->clientService,
+			$this->tempManager,
+			$this->logger
+		);
+
 		$config = \OC::$server->getConfig();
 		$this->appstore = $config->setSystemValue('appstoreenabled', true);
 		$config->setSystemValue('appstoreenabled', true);
@@ -63,4 +90,491 @@ class InstallerTest extends TestCase {
 		$this->assertTrue($isInstalled);
 		$installer->removeApp(self::$appid);
 	}
+
+	public function updateArrayProvider() {
+		return [
+			// Update available
+			[
+				[
+					[
+						'id' => 'files',
+						'releases' => [
+							[
+								'version' => '1111.0'
+							],
+						],
+					],
+				],
+				'1111.0',
+			],
+			// No update available
+			[
+				[
+					[
+						'id' => 'files',
+						'releases' => [
+							[
+								'version' => '1.0'
+							],
+						],
+					],
+				],
+				false,
+			],
+		];
+	}
+
+	/**
+	 * @dataProvider updateArrayProvider
+	 * @param array $appArray
+	 * @param string|bool $updateAvailable
+	 */
+	public function testIsUpdateAvailable(array $appArray, $updateAvailable) {
+		$this->appFetcher
+			->expects($this->once())
+			->method('get')
+			->willReturn($appArray);
+
+		$this->assertSame($updateAvailable, Installer::isUpdateAvailable('files', $this->appFetcher));
+	}
+
+	/**
+	 * @expectedException \Exception
+	 * @expectedExceptionMessage Certificate "4112" has been revoked
+	 */
+	public function testDownloadAppWithRevokedCertificate() {
+		$appArray = [
+			[
+				'id' => 'news',
+				'certificate' => '-----BEGIN CERTIFICATE-----
+MIIEAjCCAuoCAhAQMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
+VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
+MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
+dXRob3JpdHkwHhcNMTYxMDAzMTMyNDM3WhcNMjcwMTA5MTMyNDM3WjASMRAwDgYD
+VQQDDAdwYXNzbWFuMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApEt+
+KZGs+WqdZkHZflzqk+ophYWB8qB47XCzy+xdTGPFM84/9wXltRPbcQQWJJl5aOx0
+FPbsyTGhIt/IYZ2Vl0XrDRJjsaxzPcrofrwpJ2tqforXjGohl6mZUBA0ESzFiPzT
+SAZe8E14+Jk8rbF/ecrkqcWf2cTMV3Qfu9YvJo8WVs4lHc95r1F+Nalh/OLkHkzb
+fYPno2Z5cco6U7BXunFQG2gqy3wWQwmlhDxh5fwrCoFzPWm7WhwSyK+eMoSDz+Vp
+3kmtyijsqnda0zA9bfNzgW26czbJaObbnkdtDC2nfoAWXndlS/5YRI8yHd9miB5C
+u1OC8LUWToDGNa9+FOxBSj7Nk6iyjbVfRXcTqThdkVZdOOPaBRMsL9R4UYywCbhA
+yGNiQ0ahfXD8MZSb08rlQg8tAtcUZW1sYQcbtMGnu8OyC5J7N1efzv5mys4+9hBS
+5ECeyCuQTuOkF4H/XS2BMSFZWF2xh7wzhMLca+5yauDW4i8baFEv74QTeY1DADgI
+Lz29NJ6z9xYzEnPesjNrwIcJwIjV52EkdLTi+EIf83UjXLQdwDbLxu76qxqP7K0I
+oMmwbl7UNA0wzq7nmgRhvqhow5RoCaSJjTz0EYQVSa1xelwiKeJiSKj2G9Mgt5Ms
+Miuy3C3VAGvQJ2ocILPGOt54oVeNRFLpnCo1e3sCAwEAATANBgkqhkiG9w0BAQsF
+AAOCAQEAkGYtg21rGpUVT/AokGUfI0PeyYAkcXKy2yuBAzfRk+uIXnRR0vK+OMpx
+shBoYGR3JEGUHZcMTRh8wjAZ0wuyYlQONtJbFFF3bCfODXxCsw0Vm8/Ms+KCmE4Z
+SyQafWEQf1sdqNw4VS4DYS2mlpDgAl+U9UY6HQKuT3+GFIxCsQSdS0GTaiYVKPVE
+p/eKou739h+5dM4FEhIYZX+7PWlHmX6wPCFAjgNu3kiRGmF6LKmCNNXTySATEP86
+tczQMzLtVdTg5z8XMi//6TkAPxRPjYi8Vef/s2mLo7KystTmofxI/HZePSieJ9tj
+gLgK8d8sKL60JMmKHN3boHrsThKBVA==
+-----END CERTIFICATE-----',
+			],
+		];
+		$this->appFetcher
+			->expects($this->once())
+			->method('get')
+			->willReturn($appArray);
+
+
+		$this->installer->downloadApp('news');
+	}
+
+	/**
+	 * @expectedException \Exception
+	 * @expectedExceptionMessage App with id news has a certificate not issued by a trusted Code Signing Authority
+	 */
+	public function testDownloadAppWithNotNextcloudCertificate() {
+		$appArray = [
+			[
+				'id' => 'news',
+				'certificate' => '-----BEGIN CERTIFICATE-----
+MIID8TCCAdkCAhAAMA0GCSqGSIb3DQEBCwUAMG0xCzAJBgNVBAYTAlVTMQ8wDQYD
+VQQIDAZCb3N0b24xFjAUBgNVBAoMDW93bkNsb3VkIEluYy4xNTAzBgNVBAMMLG93
+bkNsb3VkIENvZGUgU2lnbmluZyBJbnRlcm1lZGlhdGUgQXV0aG9yaXR5MB4XDTE2
+MDIwMzE3NTE0OVoXDTI2MDEzMTE3NTE0OVowDzENMAsGA1UEAwwEY29yZTCCASIw
+DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPHdSljnHI+ueQd27UyWPO9n4Lqt
+bK0kdekiC3si7Mee7uXXJaGuqXJozHEZYB1LIFLdCU/itCxEk9hyLcyNzeT+nRT/
+zDuOYdbLgCj7/A5bX+u3jc29UlCYybSFchfMdvn7a0njCna4dE+73b4yEj16tS2h
+S1EUygSzgicWlJqMD3Z9Qc+zLEpdhq9oDdDB8HURi2NW4KzIraVncSH+zF1QduOh
+nERDnF8x48D3FLdTxGA0W/Kg4gYsq4NRvU6g3DJNdp4YfqRSFMmLFDCgzDuhan7D
+wgRlI9NAeHbnyoUPtrDBUceI7shIbC/i87xk9ptqV0AyFonkJtK6lWwZjNkCAwEA
+ATANBgkqhkiG9w0BAQsFAAOCAgEAAMgymqZE1YaHYlRGwvTE7gGDY3gmFOMaxQL4
+E5m0CnkBz4BdIPRsQFFdOv3l/MIWkw5ED3vUB925VpQZYFSiEuv5NbnlPaHZlIMI
+n8AV/sTP5jue3LhtAN4EM63xNBhudAT6wVsvGwOuQOx9Xv+ptO8Po7sTuNYP0CMH
+EOQN+/q8tYlSm2VW+dAlaJ+zVZwZldhVjL+lSH4E9ktWn3PmgNQeKfcnJISUbus6
+ZtsYDF/X96/Z2ZQvMXOKksgvU6XlvIxllcyebC9Bxe/h0D63GCO2tqN5CWQzIIqn
+apUynPX8BlLaaExqYGERwlUi/yOGaUVPUjEPVehviOQYgAqxlrkJk1dWeCrwUori
+CXpi+IUYkidfgiJ9F88M3ElpwqIaXp7G3/4oHBuE2u6M+L+1/vqPJeTCAWUxxpJE
+yYmM+db6D4TySFpQPENNzPS8bpR6T8w2hRumkldC42HrnyJJbpjOieTXhXzjdPvZ
+IEP9JGtkhB2du6nBF2MNAq2TqRXpcfQrQEbnQ13aV9bl+roTwwO+SOWK/wgvdOMI
+STQ0Xk0sTGlmQjPYPkibVceaWMR3sX4cNt5c33YhJys5jxHoAh42km4nN9tfykR5
+crl5lBlKjXh2GP0+omSO3x1jX4+iQPCW2TWoyKkUdLu/hGHG2w8RrTeme+kATECH
+YSu356M=
+-----END CERTIFICATE-----',
+			],
+		];
+		$this->appFetcher
+			->expects($this->once())
+			->method('get')
+			->willReturn($appArray);
+
+		$this->installer->downloadApp('news');
+	}
+
+	/**
+	 * @expectedException \Exception
+	 * @expectedExceptionMessage App with id news has a cert issued to passman
+	 */
+	public function testDownloadAppWithDifferentCN() {
+		$appArray = [
+			[
+				'id' => 'news',
+				'certificate' => '-----BEGIN CERTIFICATE-----
+MIIEAjCCAuoCAhAYMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
+VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
+MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
+dXRob3JpdHkwHhcNMTYxMDE5MTkzNTEyWhcNMjcwMTI1MTkzNTEyWjASMRAwDgYD
+VQQDDAdwYXNzbWFuMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1Jw1
+8F0DefogaLaBudGbhK2zcFIBSzxhh7dRWguZKHGE+rG00BOvFLIAo37Bfmy9WKLc
+3BFYvuFBowaVdaFOLxQJod0sOTmVMXhwoY5e3Xx+P+nsAw1/0gI10/LD1Vgl6i1u
+gMocmnbEYhKwr0NbdiQiMI9UB9Ge/51wt4WtAxwK7yJFl3+5qzvJgfX75Wt+8L1e
+Wk0LpVW23tUueJovjYZJXyAtohNaV3gwiST+QmKljCd4gwGX9abqfc76/lWtS+hI
+rKptuICc55ffH30rqVhAgCMouF/Ml5Qru8tDen5dSNtmAXz89OlDNisP+9HL4WDZ
+wvgps0mm/OYAUAQln24uXPDmAX/H2P5xIDHAa8avsqdgmHiqnLr4GYD8JYeb8GmB
+zZ38hEMjCr2F1k1h9T1+SyfRiDPDqqv1mBtcvNVc1JmZvSikMxhtQbU0C4/o2SBG
+RPCirknfPeKu8wBi6gvH4/SK0XTyuM8H58b9AKxzoo/wLbQ668+faLYyMSzCvsZD
+eeZkiO85y87Ax57WRY93arccCMaUeks/cTriNw3JrvdDyb2SeQOX9JUp0orUlC64
+AzK2xhXCpmkprVBGizT5g3brrknX6VDX1gXFAmH/daCRJAIHPX0S/ol0z9w/hCEl
+CpbiJPEphGtxqz4SfMv6IrIfneuDDKbF+w5MV/sCAwEAATANBgkqhkiG9w0BAQsF
+AAOCAQEAUKj+/GpnMn+0/u9SPHTNmX3U3Y/ldmud0CsU5ELzMf/3YPbC/qWziRik
+ewM2WyG8cwT9ayt9DxWGfu/zLv+ddyl8Wje1e/FIkRKXK0WW6OMz3e8Y45ONzpmu
+8ME75IpnMuZEqE/WayRg27dQT5QNnEe/uNLd4m9BfsQcHIx3OfHCu5Of6/BclgsJ
+VWp31zY8kcT0QN1GQxfB3eXnMyELneKCP3OH9DBhr4FUFb0vRHc8/1rdADFvSsdX
+hNm8iRq+s2n0F6OGBofYT8ZyCnDUSQAoKMTIHcz+dDGyP4BfPY5w0ZGUfuaYATvm
+cR92p/PYCFXkAKP3OO0RPlf6dXNKTw==
+-----END CERTIFICATE-----',
+			],
+		];
+		$this->appFetcher
+			->expects($this->once())
+			->method('get')
+			->willReturn($appArray);
+
+		$this->installer->downloadApp('news');
+	}
+
+	/**
+	 * @expectedException \Exception
+	 * @expectedExceptionMessage App with id passman has invalid signature
+	 */
+	public function testDownloadAppWithInvalidSignature() {
+		$appArray = [
+			[
+				'id' => 'passman',
+				'certificate' => '-----BEGIN CERTIFICATE-----
+MIIEAjCCAuoCAhAYMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
+VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
+MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
+dXRob3JpdHkwHhcNMTYxMDE5MTkzNTEyWhcNMjcwMTI1MTkzNTEyWjASMRAwDgYD
+VQQDDAdwYXNzbWFuMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1Jw1
+8F0DefogaLaBudGbhK2zcFIBSzxhh7dRWguZKHGE+rG00BOvFLIAo37Bfmy9WKLc
+3BFYvuFBowaVdaFOLxQJod0sOTmVMXhwoY5e3Xx+P+nsAw1/0gI10/LD1Vgl6i1u
+gMocmnbEYhKwr0NbdiQiMI9UB9Ge/51wt4WtAxwK7yJFl3+5qzvJgfX75Wt+8L1e
+Wk0LpVW23tUueJovjYZJXyAtohNaV3gwiST+QmKljCd4gwGX9abqfc76/lWtS+hI
+rKptuICc55ffH30rqVhAgCMouF/Ml5Qru8tDen5dSNtmAXz89OlDNisP+9HL4WDZ
+wvgps0mm/OYAUAQln24uXPDmAX/H2P5xIDHAa8avsqdgmHiqnLr4GYD8JYeb8GmB
+zZ38hEMjCr2F1k1h9T1+SyfRiDPDqqv1mBtcvNVc1JmZvSikMxhtQbU0C4/o2SBG
+RPCirknfPeKu8wBi6gvH4/SK0XTyuM8H58b9AKxzoo/wLbQ668+faLYyMSzCvsZD
+eeZkiO85y87Ax57WRY93arccCMaUeks/cTriNw3JrvdDyb2SeQOX9JUp0orUlC64
+AzK2xhXCpmkprVBGizT5g3brrknX6VDX1gXFAmH/daCRJAIHPX0S/ol0z9w/hCEl
+CpbiJPEphGtxqz4SfMv6IrIfneuDDKbF+w5MV/sCAwEAATANBgkqhkiG9w0BAQsF
+AAOCAQEAUKj+/GpnMn+0/u9SPHTNmX3U3Y/ldmud0CsU5ELzMf/3YPbC/qWziRik
+ewM2WyG8cwT9ayt9DxWGfu/zLv+ddyl8Wje1e/FIkRKXK0WW6OMz3e8Y45ONzpmu
+8ME75IpnMuZEqE/WayRg27dQT5QNnEe/uNLd4m9BfsQcHIx3OfHCu5Of6/BclgsJ
+VWp31zY8kcT0QN1GQxfB3eXnMyELneKCP3OH9DBhr4FUFb0vRHc8/1rdADFvSsdX
+hNm8iRq+s2n0F6OGBofYT8ZyCnDUSQAoKMTIHcz+dDGyP4BfPY5w0ZGUfuaYATvm
+cR92p/PYCFXkAKP3OO0RPlf6dXNKTw==
+-----END CERTIFICATE-----',
+				'releases' => [
+					[
+						'download' => 'https://example.com',
+						'signature' => 'MySignature',
+					],
+					[
+						'download' => 'https://nextcloud.com',
+					],
+				],
+			],
+		];
+		$this->appFetcher
+			->expects($this->once())
+			->method('get')
+			->willReturn($appArray);
+		$realTmpFile = \OC::$server->getTempManager()->getTemporaryFile('.tar.gz');
+		copy(__DIR__ . '/../data/testapp.tar.gz', $realTmpFile);
+		$this->tempManager
+			->expects($this->at(0))
+			->method('getTemporaryFile')
+			->with('.tar.gz')
+			->willReturn($realTmpFile);
+		$client = $this->createMock(IClient::class);
+		$client
+			->expects($this->once())
+			->method('get')
+			->with('https://example.com', ['save_to' => $realTmpFile]);
+		$this->clientService
+			->expects($this->once())
+			->method('newClient')
+			->willReturn($client);
+
+		$this->installer->downloadApp('passman');
+	}
+
+	/**
+	 * @expectedException \Exception
+	 * @expectedExceptionMessage Extracted app testapp has more than 1 folder
+	 */
+	public function testDownloadAppWithMoreThanOneFolderDownloaded() {
+		$appArray = [
+			[
+				'id' => 'testapp',
+				'certificate' => '-----BEGIN CERTIFICATE-----
+MIIEAjCCAuoCAhAbMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
+VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
+MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
+dXRob3JpdHkwHhcNMTYxMDMxMTgxNTI2WhcNMjcwMjA2MTgxNTI2WjASMRAwDgYD
+VQQDEwd0ZXN0YXBwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqa0x
+FcVa0YcO/ABqSNdbf7Bzp2PBBJzVM9gI4/HzzBKU/NY9/RibBBpNjAIWEFAbTI4j
+ilFSoxHDQ8HrboFOeKCrOIdp9ATQ8SnYVNIQ12Ym3LA/XxcG0gG0H7DeS9C0uACe
+svN8fwD1wnKnLLU9GBzO77jwYkneed85wwKG4waHd3965gxQWq0N5gnYS0TTn7Yr
+l1veRiw+ryefXvfWI0cN1WBZJ/4XAkwVlpG1HP60AunIpcwn9bfG4XCka+7x26E4
+6Hw0Ot7D7j0yzVzimJDPB2h2buEtPVd6m+oNPueVvKGta+p6cEEaHlFVh2Pa9DI+
+me3nb6aXE2kABWXav3BmK18A5Rg4ZY4VFYvmHmxkOhT/ulGZRqy6TccL/optqs52
+KQ6P0e5dfmhLeoCvJObD+ZYKv+kJCRFtX1Hve/R4IHG6XSFKUfrRjyor9b6TX2L/
+l2vV0mFjmy4g3l05vWHg1Edtq7M29S/xNA3/hF29NjBq6NoMbLGcBtFced1iK07Z
+yHLjXRZRfURP671Svqqg8pjxuDqkJ2vIj/Vpod4kF2jeiZYXcfmNKhEhxpkccSe0
+dI6p76Ne7XSUpf8yCPiSnWZLadqKZdEulcB4SlrZO2+/pycgqrqihofDrvDeWeeg
+gQyvbZZKl4ylRNj6IRKnosKLVXNqMHQxLmxLHeUCAwEAATANBgkqhkiG9w0BAQsF
+AAOCAQEALkKQwa40HfuP4Q6ShwBFJbXLyodIAXCT014kBVjReDKNl5oHtMXRjPxj
+nj9doKu+3bLNuLCv9uU3H5+t/GFogReV3Av3z/fCqJ6wHv/KX+lacj31dWXZGD8G
+z+RYibrxKkPN0V6q1mSvkg3hJOOE+/4FPIdc8PNlgratv3WS4dT8QwGSUavHW2Kx
+89nIdnwtLEFpgML/bTG0dm8BH57xER8LCYixW1VmpV6A4IsoKVsnB7KUCRTK3iUJ
+Zh8Xg8UMNrOtXc1Wx1Wmjaa4ZE9dY6/KkU2ny2UWyDHKU/9VE8QQ4HN93gxU4+H7
+cUg0V1uAxqUvKytKkMfcyPWsz/AINA==
+-----END CERTIFICATE-----',
+				'releases' => [
+					[
+						'download' => 'https://example.com',
+						'signature' => 'h8H3tUy2dDlwrV/hY/ZxqYqe8Vue+IINluLtAt1HxX2cjz3vdoVHJRINRkMYYcdz
+VlndvHyKdqJHDAACphR8tVV6EFrPermn7gEgWk7a51LbUM7sAN7RV7ijEooUo+TQ
+jNW9Ch48Wg3jvebMwWNr5t5U4MEXTP5f0YX/kxvkJoUrG3a3spt7ziEuHaq8IPvt
+Jj/JSDFhvRNpom7yNNcI1Ijoq8yC11sg7RJBNfrHdGPHPZVz2SyBiY9OcvgGSpUU
+bfvzhIZDCl/RRi5fs39jLLupAP69Ez6+jylNXEMsNwM0YL5+egSXFtkCvgOw8UBg
+ZqNZZojcS22acuvHRnoa6PDDhwHdCH+zpifXSOhSQvue5n6q+FVX6aeD1LnCQkYB
+D2wvNyZWwdADJtvDj03DKhm21g+TPy63XC94q4IqvjQ94pV8U+qrBBfkQ62NGjaC
+oOU6y5sEmQeAdVRpWVo0Hewmjp4Adoj5JRwuqCVEynTC6DXHs3HvHxYlmib1F05a
+GqEhdDmOHsxNaeJ08Hlptq5yLv3+0wEdtriVjgAZNVduHG1F1FkhPIrDHaB6pd67
+0AFvO/pZgMSHDRHD+safBgaLb5dBZ895Qvudbq3RQevVnO+YZQYZkpmjoF/+TQ7/
+YwDVP+QmNRzx72jtqAN/Kc3CvQ9nkgYhU65B95aX0xA=',
+					],
+					[
+						'download' => 'https://nextcloud.com',
+					],
+				],
+			],
+		];
+		$this->appFetcher
+			->expects($this->once())
+			->method('get')
+			->willReturn($appArray);
+		$realTmpFile = \OC::$server->getTempManager()->getTemporaryFile('.tar.gz');
+		copy(__DIR__ . '/../data/testapp1.tar.gz', $realTmpFile);
+		$this->tempManager
+			->expects($this->at(0))
+			->method('getTemporaryFile')
+			->with('.tar.gz')
+			->willReturn($realTmpFile);
+		$realTmpFolder = \OC::$server->getTempManager()->getTemporaryFolder();
+		mkdir($realTmpFolder . '/testfolder');
+		$this->tempManager
+			->expects($this->at(1))
+			->method('getTemporaryFolder')
+			->willReturn($realTmpFolder);
+		$client = $this->createMock(IClient::class);
+		$client
+			->expects($this->once())
+			->method('get')
+			->with('https://example.com', ['save_to' => $realTmpFile]);
+		$this->clientService
+			->expects($this->once())
+			->method('newClient')
+			->willReturn($client);
+
+		$this->installer->downloadApp('testapp');
+	}
+
+	/**
+	 * @expectedException \Exception
+	 * @expectedExceptionMessage App for id testapp has a wrong app ID in info.xml: testapp1
+	 */
+	public function testDownloadAppWithMismatchingIdentifier() {
+		$appArray = [
+			[
+				'id' => 'testapp',
+				'certificate' => '-----BEGIN CERTIFICATE-----
+MIIEAjCCAuoCAhAbMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
+VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
+MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
+dXRob3JpdHkwHhcNMTYxMDMxMTgxNTI2WhcNMjcwMjA2MTgxNTI2WjASMRAwDgYD
+VQQDEwd0ZXN0YXBwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqa0x
+FcVa0YcO/ABqSNdbf7Bzp2PBBJzVM9gI4/HzzBKU/NY9/RibBBpNjAIWEFAbTI4j
+ilFSoxHDQ8HrboFOeKCrOIdp9ATQ8SnYVNIQ12Ym3LA/XxcG0gG0H7DeS9C0uACe
+svN8fwD1wnKnLLU9GBzO77jwYkneed85wwKG4waHd3965gxQWq0N5gnYS0TTn7Yr
+l1veRiw+ryefXvfWI0cN1WBZJ/4XAkwVlpG1HP60AunIpcwn9bfG4XCka+7x26E4
+6Hw0Ot7D7j0yzVzimJDPB2h2buEtPVd6m+oNPueVvKGta+p6cEEaHlFVh2Pa9DI+
+me3nb6aXE2kABWXav3BmK18A5Rg4ZY4VFYvmHmxkOhT/ulGZRqy6TccL/optqs52
+KQ6P0e5dfmhLeoCvJObD+ZYKv+kJCRFtX1Hve/R4IHG6XSFKUfrRjyor9b6TX2L/
+l2vV0mFjmy4g3l05vWHg1Edtq7M29S/xNA3/hF29NjBq6NoMbLGcBtFced1iK07Z
+yHLjXRZRfURP671Svqqg8pjxuDqkJ2vIj/Vpod4kF2jeiZYXcfmNKhEhxpkccSe0
+dI6p76Ne7XSUpf8yCPiSnWZLadqKZdEulcB4SlrZO2+/pycgqrqihofDrvDeWeeg
+gQyvbZZKl4ylRNj6IRKnosKLVXNqMHQxLmxLHeUCAwEAATANBgkqhkiG9w0BAQsF
+AAOCAQEALkKQwa40HfuP4Q6ShwBFJbXLyodIAXCT014kBVjReDKNl5oHtMXRjPxj
+nj9doKu+3bLNuLCv9uU3H5+t/GFogReV3Av3z/fCqJ6wHv/KX+lacj31dWXZGD8G
+z+RYibrxKkPN0V6q1mSvkg3hJOOE+/4FPIdc8PNlgratv3WS4dT8QwGSUavHW2Kx
+89nIdnwtLEFpgML/bTG0dm8BH57xER8LCYixW1VmpV6A4IsoKVsnB7KUCRTK3iUJ
+Zh8Xg8UMNrOtXc1Wx1Wmjaa4ZE9dY6/KkU2ny2UWyDHKU/9VE8QQ4HN93gxU4+H7
+cUg0V1uAxqUvKytKkMfcyPWsz/AINA==
+-----END CERTIFICATE-----',
+				'releases' => [
+					[
+						'download' => 'https://example.com',
+						'signature' => 'h8H3tUy2dDlwrV/hY/ZxqYqe8Vue+IINluLtAt1HxX2cjz3vdoVHJRINRkMYYcdz
+VlndvHyKdqJHDAACphR8tVV6EFrPermn7gEgWk7a51LbUM7sAN7RV7ijEooUo+TQ
+jNW9Ch48Wg3jvebMwWNr5t5U4MEXTP5f0YX/kxvkJoUrG3a3spt7ziEuHaq8IPvt
+Jj/JSDFhvRNpom7yNNcI1Ijoq8yC11sg7RJBNfrHdGPHPZVz2SyBiY9OcvgGSpUU
+bfvzhIZDCl/RRi5fs39jLLupAP69Ez6+jylNXEMsNwM0YL5+egSXFtkCvgOw8UBg
+ZqNZZojcS22acuvHRnoa6PDDhwHdCH+zpifXSOhSQvue5n6q+FVX6aeD1LnCQkYB
+D2wvNyZWwdADJtvDj03DKhm21g+TPy63XC94q4IqvjQ94pV8U+qrBBfkQ62NGjaC
+oOU6y5sEmQeAdVRpWVo0Hewmjp4Adoj5JRwuqCVEynTC6DXHs3HvHxYlmib1F05a
+GqEhdDmOHsxNaeJ08Hlptq5yLv3+0wEdtriVjgAZNVduHG1F1FkhPIrDHaB6pd67
+0AFvO/pZgMSHDRHD+safBgaLb5dBZ895Qvudbq3RQevVnO+YZQYZkpmjoF/+TQ7/
+YwDVP+QmNRzx72jtqAN/Kc3CvQ9nkgYhU65B95aX0xA=',
+					],
+					[
+						'download' => 'https://nextcloud.com',
+					],
+				],
+			],
+		];
+		$this->appFetcher
+			->expects($this->once())
+			->method('get')
+			->willReturn($appArray);
+		$realTmpFile = \OC::$server->getTempManager()->getTemporaryFile('.tar.gz');
+		copy(__DIR__ . '/../data/testapp1.tar.gz', $realTmpFile);
+		$this->tempManager
+			->expects($this->at(0))
+			->method('getTemporaryFile')
+			->with('.tar.gz')
+			->willReturn($realTmpFile);
+		$realTmpFolder = \OC::$server->getTempManager()->getTemporaryFolder();
+		$this->tempManager
+			->expects($this->at(1))
+			->method('getTemporaryFolder')
+			->willReturn($realTmpFolder);
+		$client = $this->createMock(IClient::class);
+		$client
+			->expects($this->once())
+			->method('get')
+			->with('https://example.com', ['save_to' => $realTmpFile]);
+		$this->clientService
+			->expects($this->once())
+			->method('newClient')
+			->willReturn($client);
+
+		$this->installer->downloadApp('testapp');
+	}
+
+	public function testDownloadAppSuccessful() {
+		$appArray = [
+			[
+				'id' => 'testapp',
+				'certificate' => '-----BEGIN CERTIFICATE-----
+MIIEAjCCAuoCAhAbMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
+VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
+MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
+dXRob3JpdHkwHhcNMTYxMDMxMTgxNTI2WhcNMjcwMjA2MTgxNTI2WjASMRAwDgYD
+VQQDEwd0ZXN0YXBwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqa0x
+FcVa0YcO/ABqSNdbf7Bzp2PBBJzVM9gI4/HzzBKU/NY9/RibBBpNjAIWEFAbTI4j
+ilFSoxHDQ8HrboFOeKCrOIdp9ATQ8SnYVNIQ12Ym3LA/XxcG0gG0H7DeS9C0uACe
+svN8fwD1wnKnLLU9GBzO77jwYkneed85wwKG4waHd3965gxQWq0N5gnYS0TTn7Yr
+l1veRiw+ryefXvfWI0cN1WBZJ/4XAkwVlpG1HP60AunIpcwn9bfG4XCka+7x26E4
+6Hw0Ot7D7j0yzVzimJDPB2h2buEtPVd6m+oNPueVvKGta+p6cEEaHlFVh2Pa9DI+
+me3nb6aXE2kABWXav3BmK18A5Rg4ZY4VFYvmHmxkOhT/ulGZRqy6TccL/optqs52
+KQ6P0e5dfmhLeoCvJObD+ZYKv+kJCRFtX1Hve/R4IHG6XSFKUfrRjyor9b6TX2L/
+l2vV0mFjmy4g3l05vWHg1Edtq7M29S/xNA3/hF29NjBq6NoMbLGcBtFced1iK07Z
+yHLjXRZRfURP671Svqqg8pjxuDqkJ2vIj/Vpod4kF2jeiZYXcfmNKhEhxpkccSe0
+dI6p76Ne7XSUpf8yCPiSnWZLadqKZdEulcB4SlrZO2+/pycgqrqihofDrvDeWeeg
+gQyvbZZKl4ylRNj6IRKnosKLVXNqMHQxLmxLHeUCAwEAATANBgkqhkiG9w0BAQsF
+AAOCAQEALkKQwa40HfuP4Q6ShwBFJbXLyodIAXCT014kBVjReDKNl5oHtMXRjPxj
+nj9doKu+3bLNuLCv9uU3H5+t/GFogReV3Av3z/fCqJ6wHv/KX+lacj31dWXZGD8G
+z+RYibrxKkPN0V6q1mSvkg3hJOOE+/4FPIdc8PNlgratv3WS4dT8QwGSUavHW2Kx
+89nIdnwtLEFpgML/bTG0dm8BH57xER8LCYixW1VmpV6A4IsoKVsnB7KUCRTK3iUJ
+Zh8Xg8UMNrOtXc1Wx1Wmjaa4ZE9dY6/KkU2ny2UWyDHKU/9VE8QQ4HN93gxU4+H7
+cUg0V1uAxqUvKytKkMfcyPWsz/AINA==
+-----END CERTIFICATE-----',
+				'releases' => [
+					[
+						'download' => 'https://example.com',
+						'signature' => 'O5UWFRnSx4mSdEX83Uh9u7KW+Gl1OWU4uaFg6aYY19zc+lWP4rKCbAUH7Jo1Bohf
+qxQbhXs4cMqGmoL8dW4zeFUqSJCRk52LA+ciLezjPFv275q+BxEgyWOylLnbhBaz
++v6lXLaeG0J/ry8wEdg+rwP8FCYPsvKlXSVbFjgubvCR/owKJJf5iL0B93noBwBN
+jfbcxi7Kh16HAKy6f/gVZ6hf/4Uo7iEFMCPEHjidope+ejUpqbd8XhQg5/yh7TQ7
+VKR7pkdDG2eFr5c3CpaECdNg5ZIGRbQNJHBXHT/wliorWpYJtwtNAQJ4xC635gLP
+4klkKN4XtSj8bJUaJC6aaksLFgRSeKXaYAHai/XP6BkeyNzlSbsmyZk8cZbySx8F
+gVOzPok1c94UGT57FjeW5eqRjtmzbYivQdP89Ouz6et7PY69yOCqiRFQanrqzwoX
+MPLX6f5V9tCJtlH6ztmEcDROfvuVc0U3rEhqx2hphoyo+MZrPFpdcJL8KkIdMKbY
+7yQWrsV7QvAzygAOFsC0TlSNJbmMCljouUk9di4CUZ+xsQ6n6TZtE7gsdljlKjPS
+3Ys+e3V1HUaVzv8SaSmKwjRoQxQxHWLtXpJS2Yq+i+gq7LuC+aStzxAzV/h2plDW
+358picx/PobNDi71Q97+/CAOq+4wDOwhKwls7lwudIs=',
+					],
+					[
+						'download' => 'https://nextcloud.com',
+					],
+				],
+			],
+		];
+		$this->appFetcher
+			->expects($this->once())
+			->method('get')
+			->willReturn($appArray);
+		$realTmpFile = \OC::$server->getTempManager()->getTemporaryFile('.tar.gz');
+		copy(__DIR__ . '/../data/testapp.tar.gz', $realTmpFile);
+		$this->tempManager
+			->expects($this->at(0))
+			->method('getTemporaryFile')
+			->with('.tar.gz')
+			->willReturn($realTmpFile);
+		$realTmpFolder = \OC::$server->getTempManager()->getTemporaryFolder();
+		$this->tempManager
+			->expects($this->at(1))
+			->method('getTemporaryFolder')
+			->willReturn($realTmpFolder);
+		$client = $this->createMock(IClient::class);
+		$client
+			->expects($this->once())
+			->method('get')
+			->with('https://example.com', ['save_to' => $realTmpFile]);
+		$this->clientService
+			->expects($this->once())
+			->method('newClient')
+			->willReturn($client);
+
+		$this->installer->downloadApp('testapp');
+
+		$this->assertTrue(file_exists(__DIR__ . '/../../apps/testapp/appinfo/info.xml'));
+		$this->assertEquals('0.9', \OC_App::getAppVersionByPath(__DIR__ . '/../../apps/testapp/'));
+	}
 }