Skip to content

Commit 1167e3d

Browse files
authored
[bidi][js] Add authentication handlers (#14345)
* [bidi][js] Add authentication handlers * Fix imports
1 parent 9c686a5 commit 1167e3d

File tree

5 files changed

+226
-9
lines changed

5 files changed

+226
-9
lines changed

javascript/node/selenium-webdriver/bidi/network.js

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,22 @@ const { ContinueResponseParameters } = require('./continueResponseParameters')
2121
const { ContinueRequestParameters } = require('./continueRequestParameters')
2222
const { ProvideResponseParameters } = require('./provideResponseParameters')
2323

24+
const NetworkEvent = {
25+
BEFORE_REQUEST_SENT: 'network.beforeRequestSent',
26+
RESPONSE_STARTED: 'network.responseStarted',
27+
RESPONSE_COMPLETED: 'network.responseCompleted',
28+
AUTH_REQUIRED: 'network.authRequired',
29+
FETCH_ERROR: 'network.fetchError',
30+
}
31+
2432
/**
2533
* Represents all commands and events of Network module.
2634
* Described in https://w3c.github.io/webdriver-bidi/#module-network.
2735
*/
2836
class Network {
37+
#callbackId = 0
38+
#listener
39+
2940
/**
3041
* Represents a Network object.
3142
* @constructor
@@ -35,6 +46,43 @@ class Network {
3546
constructor(driver, browsingContextIds) {
3647
this._driver = driver
3748
this._browsingContextIds = browsingContextIds
49+
this.#listener = new Map()
50+
this.#listener.set(NetworkEvent.AUTH_REQUIRED, new Map())
51+
this.#listener.set(NetworkEvent.BEFORE_REQUEST_SENT, new Map())
52+
this.#listener.set(NetworkEvent.FETCH_ERROR, new Map())
53+
this.#listener.set(NetworkEvent.RESPONSE_STARTED, new Map())
54+
this.#listener.set(NetworkEvent.RESPONSE_COMPLETED, new Map())
55+
}
56+
57+
addCallback(eventType, callback) {
58+
const id = ++this.#callbackId
59+
60+
const eventCallbackMap = this.#listener.get(eventType)
61+
eventCallbackMap.set(id, callback)
62+
return id
63+
}
64+
65+
removeCallback(id) {
66+
let hasId = false
67+
for (const [, callbacks] of this.#listener) {
68+
if (callbacks.has(id)) {
69+
callbacks.delete(id)
70+
hasId = true
71+
}
72+
}
73+
74+
if (!hasId) {
75+
throw Error(`Callback with id ${id} not found`)
76+
}
77+
}
78+
79+
invokeCallbacks(eventType, data) {
80+
const callbacks = this.#listener.get(eventType)
81+
if (callbacks) {
82+
for (const [, callback] of callbacks) {
83+
callback(data)
84+
}
85+
}
3886
}
3987

4088
async init() {
@@ -75,10 +123,10 @@ class Network {
75123
* Subscribes to the 'network.authRequired' event and handles it with the provided callback.
76124
*
77125
* @param {Function} callback - The callback function to handle the event.
78-
* @returns {Promise<void>} - A promise that resolves when the subscription is successful.
126+
* @returns {Promise<number>} - A promise that resolves when the subscription is successful.
79127
*/
80128
async authRequired(callback) {
81-
await this.subscribeAndHandleEvent('network.authRequired', callback)
129+
return await this.subscribeAndHandleEvent('network.authRequired', callback)
82130
}
83131

84132
/**
@@ -97,10 +145,8 @@ class Network {
97145
} else {
98146
await this.bidi.subscribe(eventType)
99147
}
100-
await this._on(callback)
101-
}
148+
let id = this.addCallback(eventType, callback)
102149

103-
async _on(callback) {
104150
this.ws = await this.bidi.socket
105151
this.ws.on('message', (event) => {
106152
const { params } = JSON.parse(Buffer.from(event.toString()))
@@ -134,9 +180,10 @@ class Network {
134180
params.errorText,
135181
)
136182
}
137-
callback(response)
183+
this.invokeCallbacks(eventType, response)
138184
}
139185
})
186+
return id
140187
}
141188

142189
/**
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Licensed to the Software Freedom Conservancy (SFC) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The SFC licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
const network = require('../bidi/network')
19+
const { InterceptPhase } = require('../bidi/interceptPhase')
20+
const { AddInterceptParameters } = require('../bidi/addInterceptParameters')
21+
22+
class Network {
23+
#driver
24+
#network
25+
#callBackInterceptIdMap = new Map()
26+
27+
constructor(driver) {
28+
this.#driver = driver
29+
}
30+
31+
// This should be done in the constructor.
32+
// But since it needs to call async methods we cannot do that in the constructor.
33+
// We can have a separate async method that initialises the Script instance.
34+
// However, that pattern does not allow chaining the methods as we would like the user to use it.
35+
// Since it involves awaiting to get the instance and then another await to call the method.
36+
// Using this allows the user to do this "await driver.network.addAuthenticationHandler(callback)"
37+
async #init() {
38+
if (this.#network !== undefined) {
39+
return
40+
}
41+
this.#network = await network(this.#driver)
42+
}
43+
44+
async addAuthenticationHandler(username, password) {
45+
await this.#init()
46+
47+
const interceptId = await this.#network.addIntercept(new AddInterceptParameters(InterceptPhase.AUTH_REQUIRED))
48+
49+
const id = await this.#network.authRequired(async (event) => {
50+
await this.#network.continueWithAuth(event.request.request, username, password)
51+
})
52+
53+
this.#callBackInterceptIdMap.set(id, interceptId)
54+
return id
55+
}
56+
57+
async removeAuthenticationHandler(id) {
58+
await this.#init()
59+
60+
const interceptId = this.#callBackInterceptIdMap.get(id)
61+
62+
await this.#network.removeIntercept(interceptId)
63+
await this.#network.removeCallback(id)
64+
65+
this.#callBackInterceptIdMap.delete(id)
66+
}
67+
68+
async clearAuthenticationHandlers() {
69+
for (const [key, value] of this.#callBackInterceptIdMap.entries()) {
70+
await this.#network.removeIntercept(value)
71+
await this.#network.removeCallback(key)
72+
}
73+
74+
this.#callBackInterceptIdMap.clear()
75+
}
76+
}
77+
78+
module.exports = Network

javascript/node/selenium-webdriver/lib/webdriver.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const BIDI = require('../bidi')
4444
const { PinnedScript } = require('./pinnedScript')
4545
const JSZip = require('jszip')
4646
const Script = require('./script')
47+
const Network = require('./network')
4748

4849
// Capability names that are defined in the W3C spec.
4950
const W3C_CAPABILITY_NAMES = new Set([
@@ -656,6 +657,7 @@ function filterNonW3CCaps(capabilities) {
656657
*/
657658
class WebDriver {
658659
#script = undefined
660+
#network = undefined
659661
/**
660662
* @param {!(./session.Session|IThenable<!./session.Session>)} session Either
661663
* a known session or a promise that will be resolved to a session.
@@ -1116,6 +1118,16 @@ class WebDriver {
11161118
return this.#script
11171119
}
11181120

1121+
network() {
1122+
// The Network maintains state of the callbacks.
1123+
// Returning a new instance of the same driver will not work while removing callbacks.
1124+
if (this.#network === undefined) {
1125+
this.#network = new Network(this)
1126+
}
1127+
1128+
return this.#network
1129+
}
1130+
11191131
validatePrintPageParams(keys, object) {
11201132
let page = {}
11211133
let margin = {}

javascript/node/selenium-webdriver/test/bidi/network_commands_test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ suite(
130130

131131
await driver.get(Pages.logEntryAdded)
132132

133-
assert.strictEqual(counter, 1)
133+
assert.strictEqual(counter >= 1, true)
134134
})
135135

136136
it('can continue response', async function () {
@@ -145,7 +145,7 @@ suite(
145145

146146
await driver.get(Pages.logEntryAdded)
147147

148-
assert.strictEqual(counter, 1)
148+
assert.strictEqual(counter >= 1, true)
149149
})
150150

151151
it('can provide response', async function () {
@@ -160,7 +160,7 @@ suite(
160160

161161
await driver.get(Pages.logEntryAdded)
162162

163-
assert.strictEqual(counter, 1)
163+
assert.strictEqual(counter >= 1, true)
164164
})
165165
})
166166
},
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Licensed to the Software Freedom Conservancy (SFC) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The SFC licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
'use strict'
19+
20+
const assert = require('node:assert')
21+
const { Browser } = require('selenium-webdriver')
22+
const { Pages, suite } = require('../../lib/test')
23+
const until = require('selenium-webdriver/lib/until')
24+
const { By } = require('selenium-webdriver')
25+
26+
suite(
27+
function (env) {
28+
let driver
29+
30+
beforeEach(async function () {
31+
driver = await env.builder().build()
32+
})
33+
34+
afterEach(async function () {
35+
await driver.quit()
36+
})
37+
38+
describe('script()', function () {
39+
it('can add authentication handler', async function () {
40+
await driver.network().addAuthenticationHandler('genie', 'bottle')
41+
await driver.get(Pages.basicAuth)
42+
43+
await driver.wait(until.elementLocated(By.css('pre')))
44+
let source = await driver.getPageSource()
45+
assert.equal(source.includes('Access granted'), true)
46+
})
47+
48+
it('can remove authentication handler', async function () {
49+
const id = await driver.network().addAuthenticationHandler('genie', 'bottle')
50+
51+
await driver.network().removeAuthenticationHandler(id)
52+
53+
try {
54+
await driver.get(Pages.basicAuth)
55+
await driver.wait(until.elementLocated(By.css('pre')))
56+
assert.fail('Page should not be loaded')
57+
} catch (e) {
58+
assert.strictEqual(e.name, 'UnexpectedAlertOpenError')
59+
}
60+
})
61+
62+
it('can clear authentication handlers', async function () {
63+
await driver.network().addAuthenticationHandler('genie', 'bottle')
64+
65+
await driver.network().addAuthenticationHandler('bottle', 'genie')
66+
67+
await driver.network().clearAuthenticationHandlers()
68+
69+
try {
70+
await driver.get(Pages.basicAuth)
71+
await driver.wait(until.elementLocated(By.css('pre')))
72+
assert.fail('Page should not be loaded')
73+
} catch (e) {
74+
assert.strictEqual(e.name, 'UnexpectedAlertOpenError')
75+
}
76+
})
77+
})
78+
},
79+
{ browsers: [Browser.FIREFOX] },
80+
)

0 commit comments

Comments
 (0)