// Licensed to the Software Freedom Conservancy (SFC) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The SFC licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. 'use strict'; var assert = require('assert'), sinon = require('sinon'); var Capabilities = require('../../lib/capabilities').Capabilities, Command = require('../../lib/command').Command, CommandName = require('../../lib/command').Name, error = require('../../lib/error'), http = require('../../lib/http'), Session = require('../../lib/session').Session, promise = require('../../lib/promise'), WebElement = require('../../lib/webdriver').WebElement; describe('http', function() { describe('buildPath', function() { it('properly replaces path segments with command parameters', function() { var parameters = {'sessionId':'foo', 'url':'http://www.google.com'}; var finalPath = http.buildPath('/session/:sessionId/url', parameters); assert.equal(finalPath, '/session/foo/url'); assert.deepEqual(parameters, {'url':'http://www.google.com'}); }); it('handles web element references', function() { var parameters = {'sessionId':'foo', 'id': WebElement.buildId('bar')}; var finalPath = http.buildPath( '/session/:sessionId/element/:id/click', parameters); assert.equal(finalPath, '/session/foo/element/bar/click'); assert.deepEqual(parameters, {}); }); it('throws if missing a parameter', function() { assert.throws( () => http.buildPath('/session/:sessionId', {}), function(err) { return err instanceof error.InvalidArgumentError && 'Missing required parameter: sessionId' === err.message; }); assert.throws( () => http.buildPath( '/session/:sessionId/element/:id', {'sessionId': 'foo'}), function(err) { return err instanceof error.InvalidArgumentError && 'Missing required parameter: id' === err.message; }); }); it('does not match on segments that do not start with a colon', function() { assert.equal( http.buildPath('/session/foo:bar/baz', {}), '/session/foo:bar/baz'); }); }); describe('Executor', function() { let executor; let client; let send; beforeEach(function setUp() { client = new http.Client; send = sinon.stub(client, 'send'); executor = new http.Executor(client); }); describe('command routing', function() { it('rejects unrecognized commands', function() { return executor.execute(new Command('fake-name')) .then(assert.fail, err => { if (err instanceof error.UnknownCommandError && 'Unrecognized command: fake-name' === err.message) { return; } throw err; }) }); it('rejects promise if client fails to send request', function() { let error = new Error('boom'); send.returns(Promise.reject(error)); return assertFailsToSend(new Command(CommandName.NEW_SESSION)) .then(function(e) { assert.strictEqual(error, e); assertSent( 'POST', '/session', {}, [['Accept', 'application/json; charset=utf-8']]); }); }); it('can execute commands with no URL parameters', function() { var resp = JSON.stringify({sessionId: 'abc123'}); send.returns(Promise.resolve(new http.Response(200, {}, resp))); let command = new Command(CommandName.NEW_SESSION); return assertSendsSuccessfully(command).then(function(response) { assertSent( 'POST', '/session', {}, [['Accept', 'application/json; charset=utf-8']]); }); }); it('rejects commands missing URL parameters', function() { let command = new Command(CommandName.FIND_CHILD_ELEMENT). setParameter('sessionId', 's123'). // Let this be missing: setParameter('id', {'ELEMENT': 'e456'}). setParameter('using', 'id'). setParameter('value', 'foo'); assert.throws( () => executor.execute(command), function(err) { return err instanceof error.InvalidArgumentError && 'Missing required parameter: id' === err.message; }); assert.ok(!send.called); }); it('replaces URL parameters with command parameters', function() { var command = new Command(CommandName.GET). setParameter('sessionId', 's123'). setParameter('url', 'http://www.google.com'); send.returns(Promise.resolve(new http.Response(200, {}, ''))); return assertSendsSuccessfully(command).then(function(response) { assertSent( 'POST', '/session/s123/url', {'url': 'http://www.google.com'}, [['Accept', 'application/json; charset=utf-8']]); }); }); describe('uses correct URL', function() { beforeEach(() => executor = new http.Executor(client)); describe('in legacy mode', function() { test(CommandName.GET_WINDOW_SIZE, {sessionId:'s123'}, false, 'GET', '/session/s123/window/current/size'); test(CommandName.SET_WINDOW_SIZE, {sessionId:'s123', width: 1, height: 1}, false, 'POST', '/session/s123/window/current/size', {width: 1, height: 1}); test(CommandName.MAXIMIZE_WINDOW, {sessionId:'s123'}, false, 'POST', '/session/s123/window/current/maximize'); // This is consistent b/w legacy and W3C, just making sure. test(CommandName.GET, {sessionId:'s123', url: 'http://www.example.com'}, false, 'POST', '/session/s123/url', {url: 'http://www.example.com'}); }); describe('in W3C mode', function() { test(CommandName.GET_WINDOW_SIZE, {sessionId:'s123'}, true, 'GET', '/session/s123/window/size'); test(CommandName.SET_WINDOW_SIZE, {sessionId:'s123', width: 1, height: 1}, true, 'POST', '/session/s123/window/size', {width: 1, height: 1}); test(CommandName.MAXIMIZE_WINDOW, {sessionId:'s123'}, true, 'POST', '/session/s123/window/maximize'); // This is consistent b/w legacy and W3C, just making sure. test(CommandName.GET, {sessionId:'s123', url: 'http://www.example.com'}, true, 'POST', '/session/s123/url', {url: 'http://www.example.com'}); }); function test(command, parameters, w3c, expectedMethod, expectedUrl, opt_expectedParams) { it(`command=${command}`, function() { var resp = JSON.stringify({sessionId: 'abc123'}); send.returns(Promise.resolve(new http.Response(200, {}, resp))); let cmd = new Command(command).setParameters(parameters); executor.w3c = w3c; return executor.execute(cmd).then(function() { assertSent( expectedMethod, expectedUrl, opt_expectedParams || {}, [['Accept', 'application/json; charset=utf-8']]); }); }); } }); }); describe('response parsing', function() { it('extracts value from JSON response', function() { var responseObj = { 'status': error.ErrorCode.SUCCESS, 'value': 'http://www.google.com' }; var command = new Command(CommandName.GET_CURRENT_URL) .setParameter('sessionId', 's123'); send.returns(Promise.resolve( new http.Response(200, {}, JSON.stringify(responseObj)))); return executor.execute(command).then(function(response) { assertSent('GET', '/session/s123/url', {}, [['Accept', 'application/json; charset=utf-8']]); assert.strictEqual(response, 'http://www.google.com'); }); }); describe('extracts Session from NEW_SESSION response', function() { beforeEach(() => executor = new http.Executor(client)); const command = new Command(CommandName.NEW_SESSION); describe('fails if server returns invalid response', function() { describe('(empty response)', function() { test(true); test(false); function test(w3c) { it('w3c === ' + w3c, function() { send.returns(Promise.resolve(new http.Response(200, {}, ''))); executor.w3c = w3c; return executor.execute(command).then( () => assert.fail('expected to fail'), (e) => { if (!e.message.startsWith('Unable to parse')) { throw e; } }); }); } }); describe('(no session ID)', function() { test(true); test(false); function test(w3c) { it('w3c === ' + w3c, function() { let resp = {value:{name: 'Bob'}}; send.returns(Promise.resolve( new http.Response(200, {}, JSON.stringify(resp)))); executor.w3c = w3c; return executor.execute(command).then( () => assert.fail('expected to fail'), (e) => { if (!e.message.startsWith('Unable to parse')) { throw e; } }); }); } }); }); it('handles legacy response', function() { var rawResponse = {sessionId: 's123', status: 0, value: {name: 'Bob'}}; send.returns(Promise.resolve( new http.Response(200, {}, JSON.stringify(rawResponse)))); assert.ok(!executor.w3c); return executor.execute(command).then(function(response) { assert.ok(response instanceof Session); assert.equal(response.getId(), 's123'); let caps = response.getCapabilities(); assert.ok(caps instanceof Capabilities); assert.equal(caps.get('name'), 'Bob'); assert.ok(!executor.w3c); }); }); it('auto-upgrades on W3C response', function() { let rawResponse = { value: { sessionId: 's123', value: { name: 'Bob' } } }; send.returns(Promise.resolve( new http.Response(200, {}, JSON.stringify(rawResponse)))); assert.ok(!executor.w3c); return executor.execute(command).then(function(response) { assert.ok(response instanceof Session); assert.equal(response.getId(), 's123'); let caps = response.getCapabilities(); assert.ok(caps instanceof Capabilities); assert.equal(caps.get('name'), 'Bob'); assert.ok(executor.w3c); }); }); it('if w3c, does not downgrade on legacy response', function() { var rawResponse = {sessionId: 's123', status: 0, value: null}; send.returns(Promise.resolve( new http.Response(200, {}, JSON.stringify(rawResponse)))); executor.w3c = true; return executor.execute(command).then(function(response) { assert.ok(response instanceof Session); assert.equal(response.getId(), 's123'); assert.equal(response.getCapabilities().size, 0); assert.ok(executor.w3c, 'should never downgrade'); }); }); it('handles legacy new session failures', function() { let rawResponse = { status: error.ErrorCode.NO_SUCH_ELEMENT, value: {message: 'hi'} }; send.returns(Promise.resolve( new http.Response(500, {}, JSON.stringify(rawResponse)))); return executor.execute(command) .then(() => assert.fail('should have failed'), e => { assert.ok(e instanceof error.NoSuchElementError); assert.equal(e.message, 'hi'); }); }); it('handles w3c new session failures', function() { let rawResponse = {value: {error: 'no such element', message: 'oops'}}; send.returns(Promise.resolve( new http.Response(500, {}, JSON.stringify(rawResponse)))); return executor.execute(command) .then(() => assert.fail('should have failed'), e => { assert.ok(e instanceof error.NoSuchElementError); assert.equal(e.message, 'oops'); }); }); }); describe('extracts Session from DESCRIBE_SESSION response', function() { let command; beforeEach(function() { executor = new http.Executor(client); command = new Command(CommandName.DESCRIBE_SESSION) .setParameter('sessionId', 'foo'); }); describe('fails if server returns invalid response', function() { describe('(empty response)', function() { test(true); test(false); function test(w3c) { it('w3c === ' + w3c, function() { send.returns(Promise.resolve(new http.Response(200, {}, ''))); executor.w3c = w3c; return executor.execute(command).then( () => assert.fail('expected to fail'), (e) => { if (!e.message.startsWith('Unable to parse')) { throw e; } }); }); } }); describe('(no session ID)', function() { test(true); test(false); function test(w3c) { it('w3c === ' + w3c, function() { let resp = {value:{name: 'Bob'}}; send.returns(Promise.resolve( new http.Response(200, {}, JSON.stringify(resp)))); executor.w3c = w3c; return executor.execute(command).then( () => assert.fail('expected to fail'), (e) => { if (!e.message.startsWith('Unable to parse')) { throw e; } }); }); } }); }); it('handles legacy response', function() { var rawResponse = {sessionId: 's123', status: 0, value: {name: 'Bob'}}; send.returns(Promise.resolve( new http.Response(200, {}, JSON.stringify(rawResponse)))); assert.ok(!executor.w3c); return executor.execute(command).then(function(response) { assert.ok(response instanceof Session); assert.equal(response.getId(), 's123'); let caps = response.getCapabilities(); assert.ok(caps instanceof Capabilities); assert.equal(caps.get('name'), 'Bob'); assert.ok(!executor.w3c); }); }); it('does not auto-upgrade on W3C response', function() { var rawResponse = {value: {sessionId: 's123', value: {name: 'Bob'}}}; send.returns(Promise.resolve( new http.Response(200, {}, JSON.stringify(rawResponse)))); assert.ok(!executor.w3c); return executor.execute(command).then(function(response) { assert.ok(response instanceof Session); assert.equal(response.getId(), 's123'); let caps = response.getCapabilities(); assert.ok(caps instanceof Capabilities); assert.equal(caps.get('name'), 'Bob'); assert.ok(!executor.w3c); }); }); it('if w3c, does not downgrade on legacy response', function() { var rawResponse = {sessionId: 's123', status: 0, value: null}; send.returns(Promise.resolve( new http.Response(200, {}, JSON.stringify(rawResponse)))); executor.w3c = true; return executor.execute(command).then(function(response) { assert.ok(response instanceof Session); assert.equal(response.getId(), 's123'); assert.equal(response.getCapabilities().size, 0); assert.ok(executor.w3c, 'should never downgrade'); }); }); }); it('handles JSON null', function() { var command = new Command(CommandName.GET_CURRENT_URL) .setParameter('sessionId', 's123'); send.returns(Promise.resolve(new http.Response(200, {}, 'null'))); return executor.execute(command).then(function(response) { assertSent('GET', '/session/s123/url', {}, [['Accept', 'application/json; charset=utf-8']]); assert.strictEqual(response, null); }); }); describe('falsy values', function() { test(0); test(false); test(''); function test(value) { it(`value=${value}`, function() { var command = new Command(CommandName.GET_CURRENT_URL) .setParameter('sessionId', 's123'); send.returns(Promise.resolve( new http.Response(200, {}, JSON.stringify({status: 0, value: value})))); return executor.execute(command).then(function(response) { assertSent('GET', '/session/s123/url', {}, [['Accept', 'application/json; charset=utf-8']]); assert.strictEqual(response, value); }); }); } }); it('handles non-object JSON', function() { var command = new Command(CommandName.GET_CURRENT_URL) .setParameter('sessionId', 's123'); send.returns(Promise.resolve(new http.Response(200, {}, '123'))); return executor.execute(command).then(function(response) { assertSent('GET', '/session/s123/url', {}, [['Accept', 'application/json; charset=utf-8']]); assert.strictEqual(response, 123); }); }); it('returns body text when 2xx but not JSON', function() { var command = new Command(CommandName.GET_CURRENT_URL) .setParameter('sessionId', 's123'); send.returns(Promise.resolve( new http.Response(200, {}, 'hello, world\r\ngoodbye, world!'))); return executor.execute(command).then(function(response) { assertSent('GET', '/session/s123/url', {}, [['Accept', 'application/json; charset=utf-8']]); assert.strictEqual(response, 'hello, world\ngoodbye, world!'); }); }); it('returns body text when 2xx but invalid JSON', function() { var command = new Command(CommandName.GET_CURRENT_URL) .setParameter('sessionId', 's123'); send.returns(Promise.resolve( new http.Response(200, {}, '['))); return executor.execute(command).then(function(response) { assertSent('GET', '/session/s123/url', {}, [['Accept', 'application/json; charset=utf-8']]); assert.strictEqual(response, '['); }); }); it('returns null if no body text and 2xx', function() { var command = new Command(CommandName.GET_CURRENT_URL) .setParameter('sessionId', 's123'); send.returns(Promise.resolve(new http.Response(200, {}, ''))); return executor.execute(command).then(function(response) { assertSent('GET', '/session/s123/url', {}, [['Accept', 'application/json; charset=utf-8']]); assert.strictEqual(response, null); }); }); it('returns normalized body text when 2xx but not JSON', function() { var command = new Command(CommandName.GET_CURRENT_URL) .setParameter('sessionId', 's123'); send.returns(Promise.resolve(new http.Response(200, {}, '\r\n\n\n\r\n'))); return executor.execute(command).then(function(response) { assertSent('GET', '/session/s123/url', {}, [['Accept', 'application/json; charset=utf-8']]); assert.strictEqual(response, '\n\n\n\n'); }); }); it('throws UnsupportedOperationError for 404 and body not JSON', function() { var command = new Command(CommandName.GET_CURRENT_URL) .setParameter('sessionId', 's123'); send.returns(Promise.resolve( new http.Response(404, {}, 'hello, world\r\ngoodbye, world!'))); return executor.execute(command) .then( () => assert.fail('should have failed'), checkError( error.UnsupportedOperationError, 'hello, world\ngoodbye, world!')); }); it('throws WebDriverError for generic 4xx when body not JSON', function() { var command = new Command(CommandName.GET_CURRENT_URL) .setParameter('sessionId', 's123'); send.returns(Promise.resolve( new http.Response(500, {}, 'hello, world\r\ngoodbye, world!'))); return executor.execute(command) .then( () => assert.fail('should have failed'), checkError( error.WebDriverError, 'hello, world\ngoodbye, world!')) .then(function() { assertSent('GET', '/session/s123/url', {}, [['Accept', 'application/json; charset=utf-8']]); }); }); }); it('canDefineNewCommands', function() { executor.defineCommand('greet', 'GET', '/person/:name'); var command = new Command('greet'). setParameter('name', 'Bob'); send.returns(Promise.resolve(new http.Response(200, {}, ''))); return assertSendsSuccessfully(command).then(function(response) { assertSent('GET', '/person/Bob', {}, [['Accept', 'application/json; charset=utf-8']]); }); }); it('canRedefineStandardCommands', function() { executor.defineCommand(CommandName.GO_BACK, 'POST', '/custom/back'); var command = new Command(CommandName.GO_BACK). setParameter('times', 3); send.returns(Promise.resolve(new http.Response(200, {}, ''))); return assertSendsSuccessfully(command).then(function(response) { assertSent('POST', '/custom/back', {'times': 3}, [['Accept', 'application/json; charset=utf-8']]); }); }); it('accepts promised http clients', function() { executor = new http.Executor(Promise.resolve(client)); var resp = JSON.stringify({sessionId: 'abc123'}); send.returns(Promise.resolve(new http.Response(200, {}, resp))); let command = new Command(CommandName.NEW_SESSION); return executor.execute(command).then(response => { assertSent( 'POST', '/session', {}, [['Accept', 'application/json; charset=utf-8']]); }); }); function entries(map) { let entries = []; for (let e of map.entries()) { entries.push(e); } return entries; } function checkError(type, message) { return function(e) { if (e instanceof type) { assert.strictEqual(e.message, message); } else { throw e; } }; } function assertSent(method, path, data, headers) { assert.ok(send.calledWith(sinon.match(function(value) { assert.equal(value.method, method); assert.equal(value.path, path); assert.deepEqual(value.data, data); assert.deepEqual(entries(value.headers), headers); return true; }))); } function assertSendsSuccessfully(command) { return executor.execute(command).then(function(response) { return response; }); } function assertFailsToSend(command, opt_onError) { return executor.execute(command).then( () => {throw Error('should have failed')}, (e) => {return e}); } }); });