@appium/base-driver 10.0.0-beta.0 → 10.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +0 -8
  2. package/build/lib/basedriver/capabilities.d.ts.map +1 -1
  3. package/build/lib/basedriver/capabilities.js +2 -4
  4. package/build/lib/basedriver/capabilities.js.map +1 -1
  5. package/build/lib/basedriver/commands/timeout.js +7 -25
  6. package/build/lib/basedriver/commands/timeout.js.map +1 -1
  7. package/build/lib/basedriver/core.d.ts +0 -8
  8. package/build/lib/basedriver/core.d.ts.map +1 -1
  9. package/build/lib/basedriver/core.js +8 -18
  10. package/build/lib/basedriver/core.js.map +1 -1
  11. package/build/lib/basedriver/driver.js +2 -2
  12. package/build/lib/basedriver/driver.js.map +1 -1
  13. package/build/lib/basedriver/helpers.d.ts +9 -1
  14. package/build/lib/basedriver/helpers.d.ts.map +1 -1
  15. package/build/lib/basedriver/helpers.js +56 -142
  16. package/build/lib/basedriver/helpers.js.map +1 -1
  17. package/build/lib/basedriver/validation.d.ts +7 -0
  18. package/build/lib/basedriver/validation.d.ts.map +1 -0
  19. package/build/lib/basedriver/validation.js +130 -0
  20. package/build/lib/basedriver/validation.js.map +1 -0
  21. package/build/lib/express/middleware.d.ts +0 -6
  22. package/build/lib/express/middleware.d.ts.map +1 -1
  23. package/build/lib/express/middleware.js +28 -60
  24. package/build/lib/express/middleware.js.map +1 -1
  25. package/build/lib/express/server.d.ts.map +1 -1
  26. package/build/lib/express/server.js +0 -1
  27. package/build/lib/express/server.js.map +1 -1
  28. package/build/lib/helpers/capabilities.d.ts +13 -6
  29. package/build/lib/helpers/capabilities.d.ts.map +1 -1
  30. package/build/lib/helpers/capabilities.js +7 -0
  31. package/build/lib/helpers/capabilities.js.map +1 -1
  32. package/build/lib/index.d.ts +1 -0
  33. package/build/lib/index.d.ts.map +1 -1
  34. package/build/lib/index.js +3 -1
  35. package/build/lib/index.js.map +1 -1
  36. package/build/lib/jsonwp-proxy/proxy.d.ts +0 -8
  37. package/build/lib/jsonwp-proxy/proxy.d.ts.map +1 -1
  38. package/build/lib/jsonwp-proxy/proxy.js +7 -38
  39. package/build/lib/jsonwp-proxy/proxy.js.map +1 -1
  40. package/build/lib/protocol/errors.d.ts +171 -277
  41. package/build/lib/protocol/errors.d.ts.map +1 -1
  42. package/build/lib/protocol/errors.js +201 -421
  43. package/build/lib/protocol/errors.js.map +1 -1
  44. package/build/lib/protocol/helpers.d.ts +6 -6
  45. package/build/lib/protocol/helpers.d.ts.map +1 -1
  46. package/build/lib/protocol/helpers.js +11 -7
  47. package/build/lib/protocol/helpers.js.map +1 -1
  48. package/build/lib/protocol/index.d.ts +2 -1
  49. package/build/lib/protocol/index.d.ts.map +1 -1
  50. package/build/lib/protocol/index.js +2 -1
  51. package/build/lib/protocol/index.js.map +1 -1
  52. package/build/lib/protocol/protocol.d.ts +5 -0
  53. package/build/lib/protocol/protocol.d.ts.map +1 -1
  54. package/build/lib/protocol/protocol.js +23 -23
  55. package/build/lib/protocol/protocol.js.map +1 -1
  56. package/build/lib/protocol/routes.d.ts +14 -715
  57. package/build/lib/protocol/routes.d.ts.map +1 -1
  58. package/build/lib/protocol/routes.js +28 -487
  59. package/build/lib/protocol/routes.js.map +1 -1
  60. package/build/lib/protocol/validators.d.ts +4 -7
  61. package/build/lib/protocol/validators.d.ts.map +1 -1
  62. package/build/lib/protocol/validators.js +4 -21
  63. package/build/lib/protocol/validators.js.map +1 -1
  64. package/lib/basedriver/capabilities.ts +2 -4
  65. package/lib/basedriver/commands/timeout.ts +11 -34
  66. package/lib/basedriver/core.ts +10 -19
  67. package/lib/basedriver/driver.ts +3 -3
  68. package/lib/basedriver/helpers.js +61 -167
  69. package/lib/basedriver/validation.ts +145 -0
  70. package/lib/express/middleware.js +32 -70
  71. package/lib/express/server.js +0 -2
  72. package/lib/helpers/capabilities.js +9 -4
  73. package/lib/index.js +2 -0
  74. package/lib/jsonwp-proxy/proxy.js +8 -45
  75. package/lib/protocol/{errors.js → errors.ts} +322 -436
  76. package/lib/protocol/helpers.js +12 -8
  77. package/lib/protocol/index.js +8 -1
  78. package/lib/protocol/protocol.js +25 -23
  79. package/lib/protocol/routes.js +30 -497
  80. package/lib/protocol/validators.ts +19 -0
  81. package/package.json +10 -11
  82. package/build/lib/basedriver/desired-caps.d.ts +0 -5
  83. package/build/lib/basedriver/desired-caps.d.ts.map +0 -1
  84. package/build/lib/basedriver/desired-caps.js +0 -92
  85. package/build/lib/basedriver/desired-caps.js.map +0 -1
  86. package/lib/basedriver/README.md +0 -36
  87. package/lib/basedriver/desired-caps.js +0 -103
  88. package/lib/express/README.md +0 -59
  89. package/lib/jsonwp-proxy/README.md +0 -52
  90. package/lib/jsonwp-status/README.md +0 -20
  91. package/lib/protocol/README.md +0 -100
  92. package/lib/protocol/validators.js +0 -38
@@ -0,0 +1,145 @@
1
+ import type { Constraint } from '@appium/types';
2
+ import log from './logger';
3
+ import _ from 'lodash';
4
+
5
+ export class Validator {
6
+ private readonly _validators: Record<
7
+ keyof Constraint,
8
+ (value: any, options?: any, key?: string) => string | null
9
+ > = {
10
+ isString: (value: any, options?: any): string | null => {
11
+ if (_.isUndefined(value) || _.isNil(options)) {
12
+ return null;
13
+ }
14
+
15
+ if (_.isString(value)) {
16
+ return options ? null : 'must not be of type string';
17
+ }
18
+
19
+ return options ? 'must be of type string' : null;
20
+ },
21
+ isNumber: (value: any, options?: any): string | null => {
22
+ if (_.isUndefined(value) || _.isNil(options)) {
23
+ return null;
24
+ }
25
+
26
+ if (_.isNumber(value)) {
27
+ return options ? null : 'must not be of type number';
28
+ }
29
+
30
+ // allow a string value
31
+ if (options && _.isString(value) && !isNaN(Number(value))) {
32
+ log.warn('Number capability passed in as string. Functionality may be compromised.');
33
+ return null;
34
+ }
35
+
36
+ return options ? 'must be of type number' : null;
37
+ },
38
+ isBoolean: (value: any, options?: any): string | null => {
39
+ if (_.isUndefined(value) || _.isNil(options)) {
40
+ return null;
41
+ }
42
+
43
+ if (_.isBoolean(value)) {
44
+ return options ? null : 'must not be of type boolean';
45
+ }
46
+
47
+ // allow a string value
48
+ if (options && _.isString(value) && ['true', 'false', ''].includes(value)) {
49
+ return null;
50
+ }
51
+
52
+ return options ? 'must be of type boolean' : null;
53
+ },
54
+ isObject: (value: any, options?: any): string | null => {
55
+ if (_.isUndefined(value) || _.isNil(options)) {
56
+ return null;
57
+ }
58
+
59
+ if (_.isPlainObject(value)) {
60
+ return options ? null : 'must not be a plain object';
61
+ }
62
+
63
+ return options ? 'must be a plain object' : null;
64
+ },
65
+ isArray: (value: any, options?: any): string | null => {
66
+ if (_.isUndefined(value) || _.isNil(options)) {
67
+ return null;
68
+ }
69
+
70
+ if (_.isArray(value)) {
71
+ return options ? null : 'must not be of type array';
72
+ }
73
+
74
+ return options ? 'must be of type array' : null;
75
+ },
76
+ deprecated: (value: any, options?: any, key?: string): string | null => {
77
+ if (!_.isUndefined(value) && options) {
78
+ log.warn(
79
+ `The '${key}' capability has been deprecated and must not be used anymore. ` +
80
+ `Please check the driver documentation for possible alternatives.`
81
+ );
82
+ }
83
+ return null;
84
+ },
85
+ inclusion: (value: any, options?: any): string | null => {
86
+ if (_.isUndefined(value) || !options) {
87
+ return null;
88
+ }
89
+ const optionsArr = _.isArray(options) ? options : [options];
90
+ if (optionsArr.some((opt) => opt === value)) {
91
+ return null;
92
+ }
93
+ return `must be contained by ${JSON.stringify(optionsArr)}`;
94
+ },
95
+ inclusionCaseInsensitive: (value: any, options?: any): string | null => {
96
+ if (_.isUndefined(value) || !options) {
97
+ return null;
98
+ }
99
+ const optionsArr = _.isArray(options) ? options : [options];
100
+ if (optionsArr.some((opt) => _.toLower(opt) === _.toLower(value))) {
101
+ return null;
102
+ }
103
+ return `must be contained by ${JSON.stringify(optionsArr)}`;
104
+ },
105
+ presence: (value: any, options?: any): string | null => {
106
+ if (_.isUndefined(value) && options) {
107
+ return 'is required to be present';
108
+ }
109
+ if (
110
+ !options?.allowEmpty &&
111
+ ((!_.isUndefined(value) && _.isEmpty(value)) || (_.isString(value) && !_.trim(value)))
112
+ ) {
113
+ return 'must not be empty or blank';
114
+ }
115
+ return null;
116
+ }
117
+ };
118
+
119
+ validate(values: Record<string, any>, constraints: Record<string, Constraint>): Record<string, string[]> | null {
120
+ const result: Record<string, string[]> = {};
121
+ for (const [key, constraint] of _.toPairs(constraints)) {
122
+ const value = values[key];
123
+ for (const [validatorName, options] of _.toPairs(constraint)) {
124
+ if (!(validatorName in this._validators)) {
125
+ continue;
126
+ }
127
+
128
+ const validationError = this._validators[validatorName](value, options, key);
129
+ if (_.isNil(validationError)) {
130
+ continue;
131
+ }
132
+
133
+ if (key in result) {
134
+ result[key].push(validationError);
135
+ } else {
136
+ result[key] = [validationError];
137
+ }
138
+ }
139
+ }
140
+ return _.isEmpty(result) ? null : result;
141
+ }
142
+
143
+ }
144
+
145
+ export const validator = new Validator();
@@ -5,6 +5,9 @@ export {handleIdempotency} from './idempotency';
5
5
  import {match} from 'path-to-regexp';
6
6
  import {util} from '@appium/support';
7
7
  import {calcSignature} from '../helpers/session';
8
+ import {getResponseForW3CError} from '../protocol/errors';
9
+
10
+ const SESSION_ID_PATTERN = /\/session\/([^/]+)/;
8
11
 
9
12
  /**
10
13
  *
@@ -14,20 +17,16 @@ import {calcSignature} from '../helpers/session';
14
17
  * @returns {any}
15
18
  */
16
19
  export function allowCrossDomain(req, res, next) {
17
- try {
18
- res.header('Access-Control-Allow-Origin', '*');
19
- res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS, DELETE');
20
- res.header(
21
- 'Access-Control-Allow-Headers',
22
- 'Cache-Control, Pragma, Origin, X-Requested-With, Content-Type, Accept, User-Agent'
23
- );
20
+ res.header('Access-Control-Allow-Origin', '*');
21
+ res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS, DELETE');
22
+ res.header(
23
+ 'Access-Control-Allow-Headers',
24
+ 'Cache-Control, Pragma, Origin, X-Requested-With, Content-Type, Accept, User-Agent'
25
+ );
24
26
 
25
- // need to respond 200 to OPTIONS
26
- if ('OPTIONS' === req.method) {
27
- return res.sendStatus(200);
28
- }
29
- } catch (err) {
30
- log.error(`Unexpected error: ${err.stack}`);
27
+ // need to respond 200 to OPTIONS
28
+ if ('OPTIONS' === req.method) {
29
+ return res.sendStatus(200);
31
30
  }
32
31
  next();
33
32
  }
@@ -50,26 +49,6 @@ export function allowCrossDomainAsyncExecute(basePath) {
50
49
  };
51
50
  }
52
51
 
53
- /**
54
- *
55
- * @param {string} basePath
56
- * @returns {import('express').RequestHandler}
57
- */
58
- export function fixPythonContentType(basePath) {
59
- return (req, res, next) => {
60
- // hack because python client library gives us wrong content-type
61
- if (
62
- new RegExp(`^${_.escapeRegExp(basePath)}`).test(req.path) &&
63
- /^Python/.test(req.headers['user-agent'] ?? '')
64
- ) {
65
- if (req.headers['content-type'] === 'application/x-www-form-urlencoded') {
66
- req.headers['content-type'] = 'application/json; charset=utf-8';
67
- }
68
- }
69
- next();
70
- };
71
- }
72
-
73
52
  /**
74
53
  *
75
54
  * @param {import('express').Request} req
@@ -78,12 +57,17 @@ export function fixPythonContentType(basePath) {
78
57
  * @returns {any}
79
58
  */
80
59
  export function handleLogContext(req, res, next) {
81
- const requestId = util.uuidV4();
60
+ const requestId = fetchHeaderValue(req, 'x-request-id') || util.uuidV4();
82
61
 
83
62
  const sessionId = SESSION_ID_PATTERN.exec(req.url)?.[1];
84
63
  const sessionInfo = sessionId ? {sessionId, sessionSignature: calcSignature(sessionId)} : {};
64
+ const isSensitiveHeaderValue = fetchHeaderValue(req, 'x-appium-is-sensitive');
85
65
 
86
- log.updateAsyncContext({requestId, ...sessionInfo}, true);
66
+ log.updateAsyncContext({
67
+ requestId,
68
+ ...sessionInfo,
69
+ isSensitive: ['true', '1', 'yes'].includes(_.toLower(isSensitiveHeaderValue)),
70
+ }, true);
87
71
 
88
72
  return next();
89
73
  }
@@ -142,19 +126,8 @@ export function catchAllHandler(err, req, res, next) {
142
126
  }
143
127
 
144
128
  log.error(`Uncaught error: ${err.message}`);
145
- log.error('Sending generic error response');
146
- const error = errors.UnknownError;
147
- res.status(error.w3cStatus()).json(
148
- patchWithSessionId(req, {
149
- status: error.code(),
150
- value: {
151
- error: error.error(),
152
- message: `An unknown server-side error occurred while processing the command: ${err.message}`,
153
- stacktrace: err.stack,
154
- },
155
- })
156
- );
157
- log.error(err);
129
+ const [status, body] = getResponseForW3CError(err);
130
+ res.status(status).json(body);
158
131
  }
159
132
 
160
133
  /**
@@ -163,28 +136,17 @@ export function catchAllHandler(err, req, res, next) {
163
136
  */
164
137
  export function catch404Handler(req, res) {
165
138
  log.debug(`No route found for ${req.url}`);
166
- const error = errors.UnknownCommandError;
167
- res.status(error.w3cStatus()).json(
168
- patchWithSessionId(req, {
169
- status: error.code(),
170
- value: {
171
- error: error.error(),
172
- message:
173
- 'The requested resource could not be found, or a request was ' +
174
- 'received using an HTTP method that is not supported by the mapped ' +
175
- 'resource',
176
- stacktrace: '',
177
- },
178
- })
179
- );
139
+ const [status, body] = getResponseForW3CError(new errors.UnknownCommandError());
140
+ res.status(status).json(body);
180
141
  }
181
142
 
182
- const SESSION_ID_PATTERN = /\/session\/([^/]+)/;
183
-
184
- function patchWithSessionId(req, body) {
185
- const match = SESSION_ID_PATTERN.exec(req.url);
186
- if (match) {
187
- body.sessionId = match[1];
188
- }
189
- return body;
143
+ /**
144
+ * @param {import('express').Request} req
145
+ * @param {string} name
146
+ * @returns {string | undefined}
147
+ */
148
+ function fetchHeaderValue(req, name) {
149
+ return _.isArray(req.headers[name])
150
+ ? req.headers[name][0]
151
+ : req.headers[name];
190
152
  }
@@ -9,7 +9,6 @@ import log from './logger';
9
9
  import {startLogFormatter, endLogFormatter} from './express-logging';
10
10
  import {
11
11
  allowCrossDomain,
12
- fixPythonContentType,
13
12
  defaultToJSONContentType,
14
13
  catchAllHandler,
15
14
  allowCrossDomainAsyncExecute,
@@ -172,7 +171,6 @@ export function configureServer({
172
171
  app.use(allowCrossDomainAsyncExecute(basePath));
173
172
  }
174
173
  app.use(handleIdempotency);
175
- app.use(fixPythonContentType(basePath));
176
174
  app.use(defaultToJSONContentType);
177
175
  app.use(bodyParser.urlencoded({extended: true}));
178
176
  app.use(methodOverride());
@@ -2,7 +2,14 @@
2
2
 
3
3
  import _ from 'lodash';
4
4
 
5
- function isW3cCaps(caps) {
5
+ /**
6
+ * Determine whether the given agument is valid
7
+ * W3C capabilities instance.
8
+ *
9
+ * @param {any} caps
10
+ * @returns {caps is import('@appium/types').W3CCapabilities}
11
+ */
12
+ export function isW3cCaps(caps) {
6
13
  if (!_.isPlainObject(caps)) {
7
14
  return false;
8
15
  }
@@ -32,7 +39,7 @@ function isW3cCaps(caps) {
32
39
  * @param {AppiumLogger} log
33
40
  * @returns {Capabilities<C>}
34
41
  */
35
- function fixCaps(oldCaps, desiredCapConstraints, log) {
42
+ export function fixCaps(oldCaps, desiredCapConstraints, log) {
36
43
  let caps = _.clone(oldCaps);
37
44
 
38
45
  // boolean capabilities can be passed in as strings 'false' and 'true'
@@ -73,8 +80,6 @@ function fixCaps(oldCaps, desiredCapConstraints, log) {
73
80
  return caps;
74
81
  }
75
82
 
76
- export {isW3cCaps, fixCaps};
77
-
78
83
  /**
79
84
  * @typedef {import('@appium/types').Constraints} Constraints
80
85
  * @typedef {import('@appium/types').AppiumLogger} AppiumLogger
package/lib/index.js CHANGED
@@ -54,6 +54,8 @@ export {BIDI_COMMANDS} from './protocol/bidi-commands';
54
54
 
55
55
  export {generateDriverLogPrefix} from './basedriver/helpers';
56
56
 
57
+ export {isW3cCaps} from './helpers/capabilities';
58
+
57
59
  /**
58
60
  * @typedef {import('./express/server').ServerOpts} ServerOpts
59
61
  */
@@ -11,7 +11,7 @@ import {
11
11
  import {isSessionCommand, routeToCommandName} from '../protocol';
12
12
  import {MAX_LOG_BODY_LENGTH, DEFAULT_BASE_PATH, PROTOCOLS} from '../constants';
13
13
  import ProtocolConverter from './protocol-converter';
14
- import {formatResponseValue, formatStatus} from '../protocol/helpers';
14
+ import {formatResponseValue, ensureW3cResponse} from '../protocol/helpers';
15
15
  import http from 'http';
16
16
  import https from 'https';
17
17
  import { match as pathToRegexMatch } from 'path-to-regexp';
@@ -290,37 +290,6 @@ export class JWProxy {
290
290
  }
291
291
  }
292
292
 
293
- /**
294
- * @deprecated This method is not used anymore and will be removed
295
- *
296
- * @param {string} url
297
- * @param {import('@appium/types').HTTPMethod} method
298
- * @returns {string|undefined}
299
- */
300
- requestToCommandName(url, method) {
301
- /**
302
- *
303
- * @param {RegExp} pattern
304
- * @returns {string|undefined}
305
- */
306
- const extractCommandName = (pattern) => {
307
- const pathMatch = pattern.exec(url);
308
- if (pathMatch) {
309
- return routeToCommandName(pathMatch[1], method, this.reqBasePath);
310
- }
311
- };
312
- let commandName = routeToCommandName(url, method, this.reqBasePath);
313
- if (!commandName && _.includes(url, `${this.reqBasePath}/session/`)) {
314
- commandName = extractCommandName(
315
- new RegExp(`${_.escapeRegExp(this.reqBasePath)}/session/[^/]+(.+)`)
316
- );
317
- }
318
- if (!commandName && _.includes(url, this.reqBasePath)) {
319
- commandName = extractCommandName(new RegExp(`${_.escapeRegExp(this.reqBasePath)}(/.+)`));
320
- }
321
- return commandName;
322
- }
323
-
324
293
  /**
325
294
  *
326
295
  * @param {string} url
@@ -428,18 +397,13 @@ export class JWProxy {
428
397
  /** @type {import('@appium/types').HTTPMethod} */ (req.method),
429
398
  req.body
430
399
  );
431
- for (const [name, value] of _.toPairs(response.headers)) {
432
- if (!_.isNil(value)) {
433
- res.setHeader(name, _.isBoolean(value) ? String(value) : value);
434
- }
435
- }
436
400
  statusCode = response.statusCode;
437
401
  } catch (err) {
438
402
  [statusCode, resBodyObj] = getResponseForW3CError(
439
403
  isErrorType(err, errors.ProxyRequestError) ? err.getActualError() : err
440
404
  );
441
405
  }
442
- res.set('content-type', 'application/json; charset=utf-8');
406
+ res.setHeader('content-type', 'application/json; charset=utf-8');
443
407
  if (!_.isPlainObject(resBodyObj)) {
444
408
  const error = new errors.UnknownError(
445
409
  `The downstream server response with the status code ${statusCode} is not a valid JSON object: ` +
@@ -462,7 +426,7 @@ export class JWProxy {
462
426
  }
463
427
  }
464
428
  resBodyObj.value = formatResponseValue(resBodyObj.value);
465
- res.status(statusCode).send(JSON.stringify(formatStatus(resBodyObj)));
429
+ res.status(statusCode).json(ensureW3cResponse(resBodyObj));
466
430
  }
467
431
 
468
432
  /**
@@ -503,12 +467,11 @@ export class JWProxy {
503
467
  pathname = pathname.replace('/wd/hub', '');
504
468
  }
505
469
  }
506
- return _.trimEnd(
507
- match && _.isArray(match.params?.command)
508
- ? `/${match.params.command.join('/')}`
509
- : pathname,
510
- '/'
511
- );
470
+ let result = pathname;
471
+ if (match) {
472
+ result = _.isArray(match.params?.command) ? `/${match.params.command.join('/')}` : '';
473
+ }
474
+ return _.trimEnd(result, '/');
512
475
  }
513
476
  }
514
477