Files
appium/lib/basedriver/capabilities.js
T
Dan Graham 6902a9fc0a Fix problems with W3C responses (#166)
* Make malformed caps return 400 error
  * Add InvalidArgumentTypeError to errors
  * Made 'NotYetImplementedError' the 405 (Method Not Allowed) status code
  * Made errors in 'createSession' return InvalidArgumentTypeError
* Fix incorrect W3C responses
  * Add w3c status for NotImplemted and NotYetImplemented
  * Changed `getResponseForW3CStatusError` so that throws an unknown error if the exception isn't a w3c exception
  * Checks for property `w3cStatus` to confirm if it's a proper protocol-blessed exception
* Moved logic that reassigned errors into MJSONWP blocks; W3C shouldn't reassign errors because they all have proper status codes
* Added endpoint tests that check that you can create sessions in W3C and that subsequent calls to the session return the correct responses
2018-01-11 16:47:46 -08:00

203 lines
7.8 KiB
JavaScript

import _ from 'lodash';
import { validator } from './desired-caps';
import { util } from 'appium-support';
import log from './logger';
import { errors } from '../mjsonwp/errors';
// Takes primary caps object and merges it into a secondary caps object.
// (see https://www.w3.org/TR/webdriver/#dfn-merging-capabilities)
function mergeCaps (primary = {}, secondary = {}) {
let result = Object.assign({}, primary);
for (let [name, value] of _.toPairs(secondary)) {
// Overwriting is not allowed. Primary and secondary must have different properties (w3c rule 4.4)
if (!_.isUndefined(primary[name])) {
throw new errors.InvalidArgumentError(`property '${name}' should not exist on both primary (${JSON.stringify(primary)}) and secondary (${JSON.stringify(secondary)}) object`);
}
result[name] = value;
}
return result;
}
// Validates caps against a set of constraints
function validateCaps (caps, constraints = {}, opts = {}) {
let {skipPresenceConstraint} = opts;
if (!_.isPlainObject(caps)) {
throw new errors.InvalidArgumentError(`must be a JSON object`);
}
constraints = _.cloneDeep(constraints); // Defensive copy
if (skipPresenceConstraint) {
// Remove the 'presence' constraint if we're not checking for it
for (let key of _.keys(constraints)) {
delete constraints[key].presence;
}
}
let validationErrors = validator.validate(_.pickBy(caps, util.hasValue),
constraints,
{fullMessages: false});
if (validationErrors) {
let message = [];
for (let [attribute, reasons] of _.toPairs(validationErrors)) {
for (let reason of reasons) {
message.push(`'${attribute}' ${reason}`);
}
}
throw new errors.InvalidArgumentError(message.join('; '));
}
// Return caps
return caps;
}
// Standard, non-prefixed capabilities (see https://www.w3.org/TR/webdriver/#dfn-table-of-standard-capabilities)
const STANDARD_CAPS = [
'browserName',
'browserVersion',
'platformName',
'acceptInsecureCerts',
'pageLoadStrategy',
'proxy',
'setWindowRect',
'timeouts',
'unhandledPromptBehavior'
];
function isStandardCap (cap) {
return !!_.find(STANDARD_CAPS, (standardCap) => standardCap.toLowerCase() === `${cap}`.toLowerCase());
}
// If the 'appium:' prefix was provided and it's a valid capability, strip out the prefix (see https://www.w3.org/TR/webdriver/#dfn-extension-capabilities)
// (NOTE: Method is destructive and mutates contents of caps)
function stripAppiumPrefixes (caps) {
const prefix = 'appium:';
const prefixedCaps = _.filter(_.keys(caps), cap => `${cap}`.startsWith(prefix));
const badPrefixedCaps = [];
const unprefixedCaps = _.filter(_.keys(caps), (cap) => (
!cap.includes(':') && !isStandardCap(cap)
));
// Strip out the 'appium:' prefix
for (let prefixedCap of prefixedCaps) {
const strippedCapName = prefixedCap.substr(prefix.length);
// If it's standard capability that was prefixed, add it to an array of incorrectly prefixed capabilities
if (isStandardCap(strippedCapName)) {
badPrefixedCaps.push(strippedCapName);
}
// Strip out the prefix
caps[strippedCapName] = caps[prefixedCap];
delete caps[prefixedCap];
}
// If we found standard caps that were incorrectly prefixed, throw an exception (e.g.: don't accept 'appium:platformName', only accept just 'platformName')
if (badPrefixedCaps.length > 0) {
throw new errors.InvalidArgumentError(`The capabilities ${JSON.stringify(badPrefixedCaps)} are standard capabilities and should not have the "appium:" prefix`);
}
// If client provides non-prefixed, non-standard capabilities, warn them that these should be prefixed
if (unprefixedCaps.length > 0) {
log.warn(`${JSON.stringify(unprefixedCaps)} are not standard capabilities and should have an extension prefix`);
}
}
// Parse capabilities (based on https://www.w3.org/TR/webdriver/#processing-capabilities)
function parseCaps (caps, constraints = {}, shouldValidateCaps = true) {
// If capabilities request is not an object, return error (#1.1)
if (!_.isPlainObject(caps)) {
throw new errors.InvalidArgumentError('The capabilities argument was not valid for the following reason(s): "capabilities" must be a JSON object.');
}
// Let 'requiredCaps' be property named 'alwaysMatch' from capabilities request (#2) and 'allFirstMatchCaps' be property named 'firstMatch from capabilities request (#3)
let {
alwaysMatch: requiredCaps = {}, // If 'requiredCaps' is undefined, set it to an empty JSON object (#2.1)
firstMatch: allFirstMatchCaps = [{}], // If 'firstMatch' is undefined set it to a singleton list with one empty object (#3.1)
} = caps;
// Reject 'firstMatch' argument if it's not an array (#3.2)
if (!_.isArray(allFirstMatchCaps)) {
throw new errors.InvalidArgumentError('The capabilities.firstMatch argument was not valid for the following reason(s): "capabilities.firstMatch" must be a JSON array or undefined');
}
// If an empty array as provided, we'll be forgiving and make it an array of one empty object
if (allFirstMatchCaps.length === 0) {
allFirstMatchCaps.push({});
}
// Strip out the 'appium:' prefix from all
stripAppiumPrefixes(requiredCaps);
for (let firstMatchCaps of allFirstMatchCaps) {
stripAppiumPrefixes(firstMatchCaps);
}
// Validate the requiredCaps. But don't validate 'presence' because if that constraint fails on 'alwaysMatch' it could still pass on one of the 'firstMatch' keys
if (shouldValidateCaps) {
requiredCaps = validateCaps(requiredCaps, constraints, {skipPresenceConstraint: true});
}
// Remove the 'presence' constraint for any keys that are already present in 'requiredCaps'
// since we know that this constraint has already passed
let filteredConstraints = {...constraints};
let requiredCapsKeys = _.keys(requiredCaps);
for (let key of _.keys(filteredConstraints)) {
if (requiredCapsKeys.includes(key)) {
delete filteredConstraints[key];
}
}
// Validate all of the first match capabilities and return an array with only the valid caps (see spec #5)
let validationErrors = [];
let validatedFirstMatchCaps = allFirstMatchCaps.map((firstMatchCaps) => {
try {
// Validate firstMatch caps
return shouldValidateCaps ? validateCaps(firstMatchCaps, filteredConstraints) : firstMatchCaps;
} catch (e) {
validationErrors.push(e.message);
return null;
}
}).filter((caps) => !_.isNull(caps));
// Try to merge requiredCaps with first match capabilities, break once it finds its first match (see spec #6)
let matchedCaps = null;
for (let firstMatchCaps of validatedFirstMatchCaps) {
try {
matchedCaps = mergeCaps(requiredCaps, firstMatchCaps);
if (matchedCaps) {
break;
}
} catch (ign) { }
}
// Returns variables for testing purposes
return {requiredCaps, allFirstMatchCaps, validatedFirstMatchCaps, matchedCaps, validationErrors};
}
// Calls parseCaps and just returns the matchedCaps variable
function processCapabilities (caps, constraints = {}, shouldValidateCaps = true) {
const {matchedCaps, validationErrors} = parseCaps(caps, constraints, shouldValidateCaps);
// If we found an error throw an exception
if (_.isNull(matchedCaps)) {
if (_.isArray(caps.firstMatch) && caps.firstMatch.length > 1) {
// If there was more than one 'firstMatch' cap, indicate that we couldn't find a matching capabilities set and show all the errors
throw new errors.InvalidArgumentError(`Could not find matching capabilities from ${JSON.stringify(caps)}:\n ${validationErrors.join('\n')}`);
} else {
// Otherwise, just show the singular error message
throw new errors.InvalidArgumentError(validationErrors[0]);
}
}
return matchedCaps;
}
export default { parseCaps, processCapabilities, validateCaps, mergeCaps };