mirror of
https://github.com/appium/appium.git
synced 2026-10-28 12:21:20 +08:00
* 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
203 lines
7.8 KiB
JavaScript
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 };
|