diff --git a/README.md b/README.md index 6bfe5ee..6aaa8b6 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ The following tools and utilities are not mandatory, but could be used by the ap - [Mobile Native Foundation](https://github.com/MobileNativeFoundation) - [IDB](https://github.com/facebook/idb) - [AppleSimulatorUtils](https://github.com/wix/AppleSimulatorUtils) + - For `contacts`, `camera`, `faceid`, `health`, `homekit`, `notifications`, `speech` and `userTracking` permissions ### Xcode and iOS versions diff --git a/lib/extensions/permissions.js b/lib/extensions/permissions.js index 562ebf9..347cb8f 100644 --- a/lib/extensions/permissions.js +++ b/lib/extensions/permissions.js @@ -3,6 +3,7 @@ import _ from 'lodash'; import { fs, util } from '@appium/support'; import { exec } from 'teen_process'; import path from 'path'; +import B from 'bluebird'; const STATUS = Object.freeze({ UNSET: 'unset', @@ -12,6 +13,17 @@ const STATUS = Object.freeze({ }); const WIX_SIM_UTILS = 'applesimutils'; + +// `location` permission does not work with WIX/applesimutils. +// Note that except for 'contacts', the Apple's privacy command sets +// permissions properly but it kills the app process while WIX/applesimutils does not. +// In the backward compatibility perspective, +// we'd like to keep the app process as possible. +const PERMISSIONS_APPLIED_VIA_SIMCTL = [ + 'location', + 'location-always' +]; + const SERVICES = Object.freeze({ calendar: 'kTCCServiceCalendar', camera: 'kTCCServiceCamera', @@ -82,22 +94,84 @@ async function execWix (args) { /** * Sets permissions for the given application * + * @param {import('node-simctl').Simctl} simctl - node-simctl object. * @param {string} udid - udid of the target simulator device. * @param {string} bundleId - bundle identifier of the target application. - * @param {Object} permissionsMapping - An object, where keys ar service names - * and values are corresponding state values. See https://github.com/wix/AppleSimulatorUtils + * @param {Object} permissionsMapping - An object, where keys are service names + * and values are corresponding state values. Services listed in PERMISSIONS_APPLIED_VIA_SIMCTL + * will be set with `xcrun simctl privacy` command by Apple otherwise AppleSimulatorUtils by WIX. + * See the result of `xcrun simctl privacy` and https://github.com/wix/AppleSimulatorUtils * for more details on available service names and statuses. + * Note that the `xcrun simctl privacy` command kill the app process. * @throws {Error} If there was an error while changing permissions. */ -async function setAccess (udid, bundleId, permissionsMapping) { - const permissionsArg = _.toPairs(permissionsMapping) - .map((x) => `${x[0]}=${formatStatus(x[1])}`) - .join(','); - return await execWix([ - '--byId', udid, - '--bundle', bundleId, - '--setPermissions', permissionsArg, - ]); +async function setAccess (simctl, udid, bundleId, permissionsMapping) { + const /** @type {Record} */ wixPermissions = {}; + + const /** @type {string[]} */ grantPermissions = []; + const /** @type {string[]} */ revokePermissions = []; + const /** @type {string[]} */ resetPermissions = []; + + for (const serviceName in permissionsMapping) { + if (!PERMISSIONS_APPLIED_VIA_SIMCTL.includes(serviceName)) { + wixPermissions[serviceName] = permissionsMapping[serviceName]; + } else { + switch (permissionsMapping[serviceName]) { + case STATUS.YES: + grantPermissions.push(serviceName); + break; + case STATUS.NO: + revokePermissions.push(serviceName); + break; + case STATUS.UNSET: + resetPermissions.push(serviceName); + break; + default: + log.errorAndThrow(`${serviceName} does not support ${permissionsMapping[serviceName]}. Please specify 'yes', 'no' or 'unset'.`); + }; + } + } + + const /** @type {string[]} */ permissionPromises = []; + + if (!_.isEmpty(grantPermissions)) { + log.debug(`Granting ${util.pluralize('permission', grantPermissions.length, false)} for ${bundleId}: ${grantPermissions}`); + for (const action of grantPermissions) { + permissionPromises.push(simctl.grantPermission(bundleId, action)); + } + } + + if (!_.isEmpty(revokePermissions)) { + log.debug(`Revoking ${util.pluralize('permission', revokePermissions.length, false)} for ${bundleId}: ${revokePermissions}`); + for (const action of revokePermissions) { + permissionPromises.push(simctl.revokePermission(bundleId, action)); + } + } + + if (!_.isEmpty(resetPermissions)) { + log.debug(`Resetting ${util.pluralize('permission', resetPermissions.length, false)} for ${bundleId}: ${resetPermissions}`); + for (const action of resetPermissions) { + permissionPromises.push(simctl.resetPermission(bundleId, action)); + } + } + + if (!_.isEmpty(permissionPromises)) { + await B.all(permissionPromises); + } + + if (!_.isEmpty(wixPermissions)) { + log.debug(`Setting permissions for ${bundleId} wit ${WIX_SIM_UTILS} as ${JSON.stringify(wixPermissions)}`); + const permissionsArg = _.toPairs(wixPermissions) + .map((x) => `${x[0]}=${formatStatus(x[1])}`) + .join(','); + await execWix([ + '--byId', udid, + '--bundle', bundleId, + '--setPermissions', permissionsArg, + ]); + } + + return true; } /** @@ -144,9 +218,8 @@ async function getAccess (bundleId, serviceName, simDataRoot) { const extensions = {}; /** - * Sets the particular permission to the application bundle. See - * https://github.com/wix/AppleSimulatorUtils for more details on - * the available service names and statuses. + * Sets the particular permission to the application bundle. See https://github.com/wix/AppleSimulatorUtils + * or `xcrun simctl privacy` for more details on the available service names and statuses. * * @param {string} bundleId - Application bundle identifier. * @param {string} permission - Service name to be set. @@ -163,13 +236,13 @@ extensions.setPermission = async function setPermission (bundleId, permission, v * @param {string} bundleId - Application bundle identifier. * @param {Object} permissionsMapping - A mapping where kays * are service names and values are their corresponding status values. - * See https://github.com/wix/AppleSimulatorUtils + * See https://github.com/wix/AppleSimulatorUtils or `xcrun simctl privacy` * for more details on available service names and statuses. * @throws {Error} If there was an error while changing permissions. */ extensions.setPermissions = async function setPermissions (bundleId, permissionsMapping) { log.debug(`Setting access for '${bundleId}': ${JSON.stringify(permissionsMapping, null, 2)}`); - await setAccess(this.udid, bundleId, permissionsMapping); + await setAccess(this.simctl, this.udid, bundleId, permissionsMapping); }; /** diff --git a/test/functional/simulator-e2e-specs.js b/test/functional/simulator-e2e-specs.js index 99e08f2..77c1bd0 100644 --- a/test/functional/simulator-e2e-specs.js +++ b/test/functional/simulator-e2e-specs.js @@ -349,6 +349,29 @@ describe('advanced features', function () { }); }); }); + + describe('Permission', function () { + it('should set and get with simctrl privacy command', async function () { + // no exceptions + await expect(sim.setPermission('com.apple.Maps', 'location', 'yes')).not.to.be.rejected; + await expect(sim.setPermission('com.apple.Maps', 'location', 'no')).not.to.be.rejected; + await expect(sim.setPermission('com.apple.Maps', 'location', 'unset')).not.to.be.rejected; + await expect(sim.setPermission('com.apple.Maps', 'location', 'unsupported')).to.be.rejected; + }); + + it('should set and get with wix command', async function () { + await sim.setPermission('com.apple.Maps', 'contacts', 'yes'); + await sim.getPermission('com.apple.Maps', 'contacts').should.eventually.eql('yes'); + await sim.setPermission('com.apple.Maps', 'contacts', 'no'); + await sim.getPermission('com.apple.Maps', 'contacts').should.eventually.eql('no'); + + // unset sets as 'no' + await sim.setPermission('com.apple.Maps', 'contacts', 'yes'); + await sim.getPermission('com.apple.Maps', 'contacts').should.eventually.eql('yes'); + await sim.setPermission('com.apple.Maps', 'contacts', 'unset'); + await sim.getPermission('com.apple.Maps', 'contacts').should.eventually.eql('no'); + }); + }); }); describe(`multiple instances of ${OS_VERSION} simulator on Xcode9+`, function () {