fix(base-driver): Properly validate capabilities inside appium:options map
MODIFY
packages/base-driver/lib/basedriver/capabilities.js
packages/base-driver/lib/basedriver/capabilities.js
@@ -7,8 +7,7 @@ import log from './logger';
import {errors} from '../protocol/errors';
const APPIUM_VENDOR_PREFIX = 'appium:';
-const APPIUM_OPTS_CAP = 'options';
-const PREFIXED_APPIUM_OPTS_CAP = `${APPIUM_VENDOR_PREFIX}${APPIUM_OPTS_CAP}`;
+const PREFIXED_APPIUM_OPTS_CAP = `${APPIUM_VENDOR_PREFIX}options`;
/**
* Takes primary caps object and merges it into a secondary caps object.
@@ -336,43 +335,78 @@ function processCapabilities(
/**
* Return a copy of a capabilities object which has taken everything within the 'options'
- * capability and promoted it to the top level. Note that this function is assumed to be run after
- * all vendor prefixes have already been stripped from the top level. So we are dealing with e.g.
- * 'options' and not 'appium:options' at this point. Any prefixes _inside_ the 'options' capability
- * will themselves be stripped. This is designed as an internal function, not one to operate on
- * user-constructed capabilities.
+ * capability and promoted it to the top level.
*
- * @param {object} originalCaps - the capabilities to analyze and promote from 'options'
- * @return {object!} - the capabilities with 'options' promoted if necessary
+ * @template {Constraints} C
+ * @param {import('@appium/types').W3CCapabilities<C>} originalCaps
+ * @return {import('@appium/types').W3CCapabilities<C>} the capabilities with 'options' promoted if necessary
*/
function promoteAppiumOptions(originalCaps) {
- const appiumOptions = originalCaps[APPIUM_OPTS_CAP];
+ const promoteForObject = (obj) => {
+ const appiumOptions = obj[PREFIXED_APPIUM_OPTS_CAP];
if (!appiumOptions) {
- return originalCaps;
+ return obj;
}
- let caps = _.cloneDeep(originalCaps);
if (!_.isPlainObject(appiumOptions)) {
- throw new errors.SessionNotCreatedError(`The ${APPIUM_OPTS_CAP} capability must be an object`);
+ throw new errors.SessionNotCreatedError(`The ${PREFIXED_APPIUM_OPTS_CAP} capability must be an object`);
+ }
+ if (_.isEmpty(appiumOptions)) {
+ return obj;
}
- // first get rid of any prefixes inside appium:options
- const strippedAppiumOptions = stripAppiumPrefixes(appiumOptions);
+ log.debug(
+ `Found ${PREFIXED_APPIUM_OPTS_CAP} capability present; will promote items inside to caps`
+ );
+
+ /**
+ * @param {string} capName
+ */
+ const shouldAddVendorPrefix = (capName) => !capName.startsWith(APPIUM_VENDOR_PREFIX);
+ const verifyIfAcceptable = (capName) => {
+ if (!_.isString(capName)) {
+ throw new errors.SessionNotCreatedError(
+ `Capability names in ${PREFIXED_APPIUM_OPTS_CAP} must be strings. '${capName}' is unexpected`
+ );
+ }
+ if (isStandardCap(capName)) {
+ throw new errors.SessionNotCreatedError(
+ `${PREFIXED_APPIUM_OPTS_CAP} must only contain vendor-specific capabilties. '${capName}' is unexpected`
+ );
+ }
+ return capName;
+ };
+ const preprocessedOptions = _(appiumOptions)
+ .mapKeys((value, key) => verifyIfAcceptable(key))
+ .mapKeys((value, key) => shouldAddVendorPrefix(key) ? `${APPIUM_VENDOR_PREFIX}${key}` : key)
+ .value();
// warn if we are going to overwrite any keys on the base caps object
- const overwrittenKeys = _.intersection(Object.keys(caps), Object.keys(strippedAppiumOptions));
+ const overwrittenKeys = _.intersection(Object.keys(obj), Object.keys(preprocessedOptions));
if (overwrittenKeys.length > 0) {
log.warn(
`Found capabilities inside ${PREFIXED_APPIUM_OPTS_CAP} that will overwrite ` +
`capabilities at the top level: ${JSON.stringify(overwrittenKeys)}`
);
}
+ return _.cloneDeep({
+ ...(_.omit(obj, PREFIXED_APPIUM_OPTS_CAP)),
+ ...preprocessedOptions
+ });
+ };
- // now just apply them to the main caps object
- caps = {...caps, ...strippedAppiumOptions};
-
- // and remove all traces of the options cap
- delete caps[APPIUM_OPTS_CAP];
- return caps;
+ const {alwaysMatch, firstMatch} = originalCaps;
+ const result = {};
+ if (_.isPlainObject(alwaysMatch)) {
+ result.alwaysMatch = promoteForObject(alwaysMatch);
+ } else if ('alwaysMatch' in originalCaps) {
+ result.alwaysMatch = alwaysMatch;
+ }
+ if (_.isArray(firstMatch)) {
+ result.firstMatch = firstMatch.map(promoteForObject);
+ } else if ('firstMatch' in originalCaps) {
+ result.firstMatch = firstMatch;
+ }
+ return result;
}
export {
@@ -381,7 +415,6 @@ export {
validateCaps,
mergeCaps,
APPIUM_VENDOR_PREFIX,
- APPIUM_OPTS_CAP,
findNonPrefixedCaps,
isStandardCap,
stripAppiumPrefixes,

MODIFY
packages/base-driver/lib/basedriver/driver.js
packages/base-driver/lib/basedriver/driver.js
import {
validateCaps,
- APPIUM_OPTS_CAP,
PREFIXED_APPIUM_OPTS_CAP,
processCapabilities,
promoteAppiumOptions,
@@ -297,16 +296,10 @@ export class BaseDriverCore extends DriverCore {
let caps;
try {
caps = processCapabilities(
- originalCaps,
+ promoteAppiumOptions(originalCaps),
this._desiredCapConstraints,
this.shouldValidateCaps
);
- if (caps[APPIUM_OPTS_CAP]) {
- this.log.debug(
- `Found ${PREFIXED_APPIUM_OPTS_CAP} capability present; will promote items inside to caps`
- );
- caps = promoteAppiumOptions(caps);
- }
caps = fixCaps(caps, this._desiredCapConstraints, this.log);
} catch (e) {
throw new errors.SessionNotCreatedError(e.message);

MODIFY
packages/base-driver/test/unit/basedriver/capabilities.spec.js
packages/base-driver/test/unit/basedriver/capabilities.spec.js
import {
+ APPIUM_VENDOR_PREFIX,
parseCaps,
validateCaps,
mergeCaps,
processCapabilities,
findNonPrefixedCaps,
+ PREFIXED_APPIUM_OPTS_CAP,
promoteAppiumOptions,
- APPIUM_OPTS_CAP,
stripAppiumPrefixes,
} from '../../../lib/basedriver/capabilities';
import _ from 'lodash';
@@ -540,67 +541,77 @@ describe('caps', function () {
});
describe('#promoteAppiumOptions', function () {
- const appiumCaps = {
- 'appium:platformVersion': '14.4',
- 'appium:deviceName': 'iPhone 11',
- 'appium:app': '/foo/bar.app.zip',
- 'appium:automationName': 'XCUITest',
+ const nonPrefixedAppiumCaps = {
+ platformVersion: '14.4',
+ deviceName: 'iPhone 11',
+ app: '/foo/bar.app.zip',
+ automationName: 'XCUITest',
};
- const nonAppiumCaps = {
+ const appiumCaps = _.mapKeys(nonPrefixedAppiumCaps, (value, key) => `${APPIUM_VENDOR_PREFIX}${key}`);
+ const standardCaps = {
platformName: 'iOS',
};
- const appiumUnprefixedCaps = stripAppiumPrefixes(appiumCaps);
- const simpleCaps = {
- ...nonAppiumCaps,
- ...appiumUnprefixedCaps,
- };
it('should do nothing to caps that dont include the options', function () {
- promoteAppiumOptions(simpleCaps).should.eql(simpleCaps);
+ promoteAppiumOptions({
+ alwaysMatch: {
+ ...standardCaps,
+ ...appiumCaps,
+ }
+ }).should.eql({
+ alwaysMatch: {
+ ...standardCaps,
+ ...appiumCaps,
+ }
});
- it('should promote options', function () {
- const caps = {
- ...nonAppiumCaps,
- [APPIUM_OPTS_CAP]: {
- ...appiumUnprefixedCaps,
- },
- };
- promoteAppiumOptions(caps).should.eql(simpleCaps);
});
- it('should get rid of prefixes if caps inside options are prefixed', function () {
- const caps = {
- ...nonAppiumCaps,
- [APPIUM_OPTS_CAP]: {
+ it('should promote options', function () {
+ promoteAppiumOptions({
+ alwaysMatch: {
+ ...standardCaps,
+ [PREFIXED_APPIUM_OPTS_CAP]: {
+ ...nonPrefixedAppiumCaps
+ }
+ }
+ }).should.eql({
+ alwaysMatch: {
+ ...standardCaps,
...appiumCaps,
- },
- };
- promoteAppiumOptions(caps).should.eql(simpleCaps);
+ }
+ });
});
- it('should preserve non-appium vendor prefixes', function () {
- const googCaps = {'goog:chromeOptions': {foo: 'bar'}};
- const caps = {
- ...nonAppiumCaps,
- [APPIUM_OPTS_CAP]: {
+ it('should promote options inside firstMatch', function () {
+ promoteAppiumOptions({
+ alwaysMatch: {},
+ firstMatch: [{
+ ...standardCaps,
+ [PREFIXED_APPIUM_OPTS_CAP]: {
+ ...nonPrefixedAppiumCaps
+ }
+ }]
+ }).should.eql({
+ alwaysMatch: {},
+ firstMatch: [{
+ ...standardCaps,
...appiumCaps,
- ...googCaps,
- },
- };
- promoteAppiumOptions(caps).should.eql({
- ...simpleCaps,
- ...googCaps,
+ }]
});
});
it('should overwrite caps found on the top level', function () {
- const caps = {
- ...nonAppiumCaps,
- foo: 'bar',
- [APPIUM_OPTS_CAP]: {
+ promoteAppiumOptions({
+ alwaysMatch: {
+ ...standardCaps,
+ 'appium:foo': 'bar',
+ [PREFIXED_APPIUM_OPTS_CAP]: {
+ ...nonPrefixedAppiumCaps,
+ 'foo': 'baz',
+ }
+ }
+ }).should.eql({
+ alwaysMatch: {
+ ...standardCaps,
...appiumCaps,
- foo: 'baz',
- },
- };
- promoteAppiumOptions(caps).should.eql({
- ...simpleCaps,
- foo: 'baz',
+ 'appium:foo': 'baz',
+ }
});
});
});

fix(base-driver): Properly validate capabilities inside appium:options map (#17781)