diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index a597de0..58c5da9 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -36,7 +36,11 @@ jobs: - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: "${{ matrix.xcodeVersion }}" - - run: xcrun simctl list devices available + - run: | + brew update + brew tap wix/brew + brew install applesimutils + xcrun simctl list devices available name: Install Utilities - run: npm install name: Install dev dependencies diff --git a/NOTICE.txt b/NOTICE.txt index a703b14..8b49ebd 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1,2 +1,5 @@ +This product uses [applesimutils](https://github.com/wix/AppleSimulatorUtils) software written by +Copyright 2017 Wix Engineering under MIT license + This product uses [idb](https://github.com/facebook/idb) software written by Copyright (c) 2019-present, Facebook, Inc. under MIT License diff --git a/README.md b/README.md index c453128..6aaa8b6 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ 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 aa54ec1..77b7cae 100644 --- a/lib/extensions/permissions.js +++ b/lib/extensions/permissions.js @@ -1,7 +1,9 @@ import log from '../logger'; 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', @@ -10,6 +12,22 @@ const STATUS = Object.freeze({ LIMITED: 'limited', }); +const WIX_SIM_UTILS = 'applesimutils'; + +const WIX_SERVICE_NAMES = [ + // 'contacts' permission does not work well with the privacy command while the help description addresses the command support 'contacts' + // Confirmed up to with Xcode 15 and iOS 17 + `contacts`, + + // Below services have not supported by the privacy command by Apple yet. + 'camera', + 'faceid', + 'health', + 'homekit', + 'notifications', + 'speech', + 'userTracking' +]; const SERVICES = Object.freeze({ calendar: 'kTCCServiceCalendar', @@ -35,6 +53,10 @@ function toInternalServiceName (serviceName) { ); } +function formatStatus (status) { + return [STATUS.UNSET, STATUS.NO].includes(status) ? _.toUpper(status) : status; +} + /** * Runs a command line sqlite3 query * @@ -53,51 +75,104 @@ async function execSQLiteQuery (db, query) { } } +async function execWix (args) { + try { + await fs.which(WIX_SIM_UTILS); + } catch (e) { + throw new Error( + `${WIX_SIM_UTILS} binary has not been found in your PATH. ` + + `Please install it ('brew tap wix/brew && brew install wix/brew/applesimutils') to ` + + `be able to change application permissions` + ); + } + + log.debug(`Executing: ${WIX_SIM_UTILS} ${util.quote(args)}`); + try { + const {stdout} = await exec(WIX_SIM_UTILS, args); + log.debug(`Command output: ${stdout}`); + return stdout; + } catch (e) { + throw new Error(`Cannot execute "${WIX_SIM_UTILS} ${util.quote(args)}". Original error: ${e.stderr || e.message}`); + } +} + /** * 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 are service names - * and values are corresponding state values. See the result of `xcrun simctl privacy` + * and values are corresponding state values. Services listed in WIX_SERVICE_NAMES + * will be set by WIX_SIM_UTILS otherwise via `xcrun simctl privacy` command by Apple. + * See the result of `xcrun simctl privacy` and https://github.com/wix/AppleSimulatorUtils * for more details on available service names and statuses. * @throws {Error} If there was an error while changing permissions. */ -async function setAccess (simctl, bundleId, permissionsMapping) { +async function setAccess (simctl, udid, bundleId, permissionsMapping) { + const wixPermissions = {}; + const grantPermissions = []; const revokePermissions = []; const resetPermissions = []; for (const serviceName in permissionsMapping) { - 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.warn(`${serviceName} does not support ${permissionsMapping[serviceName]}. Please specify yes, no or reset.`); - }; + if (WIX_SERVICE_NAMES.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.warn(`${serviceName} does not support ${permissionsMapping[serviceName]}. Please specify yes, no or unset.`); + }; + } } - log.debug(`Granting permissions for ${bundleId} as ${JSON.stringify(grantPermissions)}`); - log.debug(`Revoking permissions for ${bundleId} as ${JSON.stringify(revokePermissions)}`); - log.debug(`Resetting permissions for ${bundleId} as ${JSON.stringify(resetPermissions)}`); + let permissionPromises = []; - for (const action of grantPermissions) { - await simctl.grantPermission(bundleId, action); + if (!_.isEmpty(grantPermissions)) { + log.debug(`Granting permissions for ${bundleId} as ${JSON.stringify(grantPermissions)}`); + for (const action of grantPermissions) { + permissionPromises.push(simctl.grantPermission(bundleId, action)); + } } - for (const action of revokePermissions) { - await simctl.revokePermission(bundleId, action); + if (!_.isEmpty(revokePermissions)) { + log.debug(`Revoking permissions for ${bundleId} as ${JSON.stringify(grantPermissions)}`); + for (const action of revokePermissions) { + permissionPromises.push(simctl.revokePermission(bundleId, action)); + } } - for (const action of resetPermissions) { - await simctl.resetPermission(bundleId, action); + if (!_.isEmpty(resetPermissions)) { + log.debug(`Revoking permissions for ${bundleId} as ${JSON.stringify(resetPermissions)}`); + for (const action of resetPermissions) { + permissionPromises.push(simctl.resetPermission(bundleId, action)); + } + } + + if (!_.isEmpty(permissionPromises)) { + 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; @@ -147,8 +222,8 @@ async function getAccess (bundleId, serviceName, simDataRoot) { const extensions = {}; /** - * Sets the particular permission to the application bundle. See - * xcrun simctl privacy 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. @@ -165,12 +240,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 `xcrun simctl privacy` for more details on available service names and statuses. + * 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.simctl, 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 e7cfe39..9e5a494 100644 --- a/test/functional/simulator-e2e-specs.js +++ b/test/functional/simulator-e2e-specs.js @@ -351,7 +351,7 @@ describe('advanced features', function () { }); describe('Permission', function () { - it('should set and get', async function () { + it('should set and get with simctrl privacy command', async function () { await sim.setPermission('com.apple.Maps', 'microphone', 'yes'); await sim.getPermission('com.apple.Maps', 'microphone').should.eventually.eql('yes'); await sim.setPermission('com.apple.Maps', 'microphone', 'no'); @@ -359,6 +359,19 @@ describe('advanced features', function () { await sim.setPermission('com.apple.Maps', 'microphone', 'unset'); await sim.getPermission('com.apple.Maps', 'microphone').should.eventually.eql('unset'); }); + + 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'); + }); }); });