diff --git a/.eslintrc.yml b/.eslintrc.yml index b7dc0c8..9412df7 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -6,8 +6,11 @@ env: node: true jasmine: true overrides: - - files: - - "*.js" + - files: ["test/**/*.js"] + rules: + no-console: "off" + # - files: + # - "*.js" rules: # Exemplo de regras personalizadas para arquivos JavaScript semi: [2, 'always'] # Verificar se há ponto e vírgula ausente diff --git a/package.json b/package.json index 7b879f3..298d084 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-red-contrib-oauth2", - "version": "5.2.7", + "version": "6.0.0", "description": "The node-red-contrib-oauth2 is a Node-RED node that provides an OAuth2 authentication flow. This node uses the OAuth2 protocol to obtain an access token, which can be used to make authenticated API requests.", "author": "Marcos Caputo ", "contributors": [ @@ -35,7 +35,7 @@ }, "dependencies": { "axios": ">=1.3.3", - "json-schema": ">=0.4.0" + "mocha": "^10.4.0" }, "devDependencies": { "@babel/eslint-parser": "^7.21.8", @@ -49,7 +49,11 @@ "eslint-plugin-promise": "^6.1.1", "jsdoc": "^4.0.3", "json-schema": ">=0.4.0", - "prettier": "^2.8.8" + "nock": "^13.5.4", + "node-red": "^3.1.9", + "node-red-node-test-helper": "^0.3.4", + "prettier": "^2.8.8", + "should": "^13.2.3" }, "eslintConfig": { "extends": "./.eslintrc.yml" @@ -58,6 +62,7 @@ "fix": "npx eslint ./src/. && npx eslint ./src/. --fix", "lint": "prettier --plugin-search-dir . --check ./src/. && npx eslint ./src/.", "format": "prettier --plugin-search-dir . --write ./src/.", - "doc": "jsdoc -c jsdoc.json" + "doc": "jsdoc -c jsdoc.json", + "test": "mocha \"test/**/*_spec.js\"" } } diff --git a/src/locales/en-US/oauth2.json b/src/locales/en-US/oauth2.json index d543d2d..476d8cb 100644 --- a/src/locales/en-US/oauth2.json +++ b/src/locales/en-US/oauth2.json @@ -13,6 +13,9 @@ "password": "Password", "client_id": "Client ID", "client_secret": "Client Secret", + "access_type": "Access Type", + "response_type": "Response Type", + "prompt": "Prompt", "scope": "Scope", "resource": "Resource", "state": "State", @@ -35,6 +38,9 @@ "password": "admin", "client_id": "012493af6282be51660dbc8e21a8462e", "client_secret": "5621bd4b5a8b09ed31817efb8d54fda2c72bfc1c6968cd4563d83f7cc26f68f6", + "access_type": "offline", + "response_type": "code", + "prompt": "consent", "scope": "scope", "resource": "resource", "state": "state", @@ -45,6 +51,7 @@ "client_credentials": "Client Credentials", "password_credentials": "Password", "authorization_code": "Authorization Code", + "implicit_flow": "Implicit Flow", "set_by_credentials": "- Set by msg.oauth2Request -" } } diff --git a/src/oauth2.html b/src/oauth2.html index 2fba7bb..6835e1f 100644 --- a/src/oauth2.html +++ b/src/oauth2.html @@ -16,6 +16,7 @@ + @@ -52,6 +53,22 @@ + +
+ + +
+ +
+ + +
+ +
+ + +
+
@@ -147,7 +164,7 @@ icon: 'red/images/typedInput/az.svg' } ]; - + RED.nodes.registerType('oauth2', { category: 'DevSecOps', color: '#fff', @@ -157,12 +174,15 @@ grant_type: { value: 'set_by_credentials' }, access_token_url: { value: '' }, authorization_endpoint: { value: '' }, - redirect_uri: { value: '/oauth2/redirect_uri' }, + redirect_uri: { value: '' }, open_authentication: { value: '' }, username: { value: '' }, password: { value: '' }, client_id: { value: '' }, client_secret: { value: '' }, + response_type: { value: '' }, + access_type: { value: '' }, + prompt: { value: '' }, scope: { value: '' }, resource: { value: '' }, state: { value: '' }, @@ -202,7 +222,9 @@ callback = `${location.protocol}//${location.hostname}${location.port ? ':' + location.port : ''}${pathname}oauth2/auth/callback`; } + // TODO - Aqui nasce o MOSTRO, está feio mas funciona! const redirectUri = `${location.protocol}//${location.hostname}${location.port ? ':' + location.port : ''}${pathname}oauth2/redirect`; + this.redirect_uri = redirectUri; if (this.container === undefined) { $('#node-input-container').val('payload'); @@ -214,18 +236,46 @@ }); const elementMapping = { - 'set_by_credentials': ['#node-rejectUnauthorized', '#node-client_credentials_in_body'], - 'client_credentials': ['#node-access_token_url', '#node-client_id', '#node-client_secret', '#node-scope', '#node-resource', '#node-state', '#node-rejectUnauthorized', '#node-client_credentials_in_body'], - 'password': ['#node-password_credentials', '#node-access_token_url', '#node-client_id', '#node-client_secret', '#node-scope', '#node-resource', '#node-state', '#node-rejectUnauthorized', '#node-client_credentials_in_body'], - 'authorization_code': ['#node-open_authentication', '#node-redirect_uri', '#node-access_token_url', '#node-authorization_endpoint', '#node-client_id', '#node-client_secret', '#node-scope', '#node-resource', '#node-state', '#node-rejectUnauthorized', '#node-client_credentials_in_body'] + set_by_credentials: ['#node-rejectUnauthorized', '#node-client_credentials_in_body'], + client_credentials: ['#node-access_token_url', '#node-client_id', '#node-client_secret', '#node-scope', '#node-resource', '#node-state', '#node-rejectUnauthorized', '#node-client_credentials_in_body'], + password: ['#node-password_credentials', '#node-access_token_url', '#node-client_id', '#node-client_secret', '#node-scope', '#node-resource', '#node-state', '#node-rejectUnauthorized', '#node-client_credentials_in_body'], + authorization_code: [ + '#node-open_authentication', + '#node-redirect_uri', + '#node-access_token_url', + '#node-authorization_endpoint', + '#node-client_id', + '#node-client_secret', + '#node-scope', + '#node-resource', + '#node-state', + '#node-rejectUnauthorized', + '#node-client_credentials_in_body' + ], + implicit_flow: [ + '#node-open_authentication', + '#node-redirect_uri', + '#node-access_token_url', + '#node-authorization_endpoint', + '#node-client_id', + '#node-client_secret', + '#node-access_type', + '#node-response_type', + '#node-prompt', + '#node-scope', + '#node-resource', + '#node-state', + '#node-rejectUnauthorized', + '#node-client_credentials_in_body' + ] }; function updateVisibility() { const grantType = $('#node-input-grant_type').val(); for (const key in elementMapping) { - elementMapping[key].forEach(selector => $(selector).hide()); + elementMapping[key].forEach((selector) => $(selector).hide()); } - elementMapping[grantType].forEach(selector => $(selector).show()); + elementMapping[grantType].forEach((selector) => $(selector).show()); RED.tray.resize(); } @@ -246,17 +296,24 @@ $('#authorizeButton').mousedown(function () { const authorizationEndpoint = $('#node-input-authorization_endpoint').val(); const clientId = $('#node-input-client_id').val(); - const clientSecret = $('#node-input-client_secret').val(); const proxy = $('#node-input-proxy').val(); - let scope = $('#node-input-scope').val().replace(/\n/g, '%20'); - let resource = $('#node-input-resource').val().replace(/\n/g, '%20'); - let state = $('#node-input-state').val().replace(/\n/g, '%20'); - let url; + const scope = $('#node-input-scope').val().replace(/\n/g, '%20'); + const resource = $('#node-input-resource').val().replace(/\n/g, '%20'); + const state = $('#node-input-state').val().replace(/\n/g, '%20'); - if (authorizationEndpoint) { - url = `oauth2/auth?id=${encodeURIComponent(id)}&clientId=${encodeURIComponent(clientId)}&clientSecret=${encodeURIComponent(clientSecret)}&scope=${encodeURIComponent(scope)}&state=${encodeURIComponent(state)}&resource=${encodeURIComponent(resource)}&callback=${encodeURIComponent(callback)}&authorizationEndpoint=${encodeURIComponent(authorizationEndpoint)}&redirectUri=${encodeURIComponent(redirectUri)}&proxy=${encodeURIComponent(proxy)}`; - } else { - url = `oauth2/auth?id=${encodeURIComponent(id)}&clientId=${encodeURIComponent(clientId)}&clientSecret=${encodeURIComponent(clientSecret)}&scope=${encodeURIComponent(scope)}&state=${encodeURIComponent(state)}&resource=${encodeURIComponent(resource)}&callback=${encodeURIComponent(callback)}&proxy=${encodeURIComponent(proxy)}`; + const accessType = $('#node-input-access_type').val(); + const responseType = $('#node-input-response_type').val(); + const prompt = $('#node-input-prompt').val(); + + let url; + if (accessType) { + url = `${authorizationEndpoint}?id=${encodeURIComponent(id)}&redirect_uri=${encodeURIComponent(redirectUri)}&client_id=${encodeURIComponent(clientId)}&response_type=${responseType}&scope=${encodeURIComponent( + scope + )}&access_type=${encodeURIComponent(accessType)}&prompt=${encodeURIComponent(prompt)}&state=${encodeURIComponent(id)}:node_id`; + } else if (authorizationEndpoint) { + url = `${authorizationEndpoint}?id=${encodeURIComponent(id)}&redirect_uri=${encodeURIComponent(redirectUri)}&client_id=${encodeURIComponent(clientId)}&response_type=code&scope=${encodeURIComponent(scope)}&resource=${encodeURIComponent( + resource + )}&state=${encodeURIComponent(id)}:node_id`; } $(this).attr('href', url); window.configNodeIntervalId = window.setTimeout(pollCredentials, 5000); @@ -292,25 +349,27 @@ .css('min-width', '450px') .editableList({ addItem: function (container, i, header) { - const row = $('
') - .css({ overflow: 'hidden', whiteSpace: 'nowrap' }) - .appendTo(container); + const row = $('
').css({ overflow: 'hidden', whiteSpace: 'nowrap' }).appendTo(container); const propertyName = $('', { class: 'node-input-header-name', type: 'text', style: 'width: 50%' - }).appendTo(row).typedInput({ types: headerTypes }); + }) + .appendTo(row) + .typedInput({ types: headerTypes }); const propertyValue = $('', { class: 'node-input-header-value', type: 'text', style: 'margin-left: 10px; width: 45%;' - }).appendTo(row).typedInput({ - types: header.h === 'content-type' ? contentTypes : [{ value: 'other', label: RED._('node-red:httpin.label.other'), hasValue: true, icon: 'red/images/typedInput/az.svg' }] - }); + }) + .appendTo(row) + .typedInput({ + types: header.h === 'content-type' ? contentTypes : [{ value: 'other', label: RED._('node-red:httpin.label.other'), hasValue: true, icon: 'red/images/typedInput/az.svg' }] + }); - const matchedType = headerTypes.filter(ht => ht.value === header.h); + const matchedType = headerTypes.filter((ht) => ht.value === header.h); if (matchedType.length === 0) { propertyName.typedInput('type', 'other'); propertyName.typedInput('value', header.h); @@ -318,7 +377,7 @@ } else { propertyName.typedInput('type', header.h); if (header.h === 'content-type') { - const matchedContentType = contentTypes.filter(ct => ct.value === header.v); + const matchedContentType = contentTypes.filter((ct) => ct.value === header.v); if (matchedContentType.length === 0) { propertyValue.typedInput('type', 'other'); propertyValue.typedInput('value', header.v); @@ -359,24 +418,26 @@ } const headers = $('#node-input-headers-container').editableList('items'); this.headers = {}; - headers.each(function () { - const header = $(this); - const keyType = header.find('.node-input-header-name').typedInput('type'); - const keyValue = header.find('.node-input-header-name').typedInput('value'); - const valueType = header.find('.node-input-header-value').typedInput('type'); - const valueValue = header.find('.node-input-header-value').typedInput('value'); - let key = keyType; - let value = valueType; - if (keyType === 'other') { - key = keyValue; - } - if (valueType === 'other') { - value = valueValue; - } - if (key !== '') { - this.headers[key] = value; - } - }.bind(this)); + headers.each( + function () { + const header = $(this); + const keyType = header.find('.node-input-header-name').typedInput('type'); + const keyValue = header.find('.node-input-header-name').typedInput('value'); + const valueType = header.find('.node-input-header-value').typedInput('type'); + const valueValue = header.find('.node-input-header-value').typedInput('value'); + let key = keyType; + let value = valueType; + if (keyType === 'other') { + key = keyValue; + } + if (valueType === 'other') { + value = valueValue; + } + if (key !== '') { + this.headers[key] = value; + } + }.bind(this) + ); }, oneditresize: function (size) { const dlg = $('#dialog-form'); @@ -394,4 +455,3 @@ }); })(); - diff --git a/src/oauth2.js b/src/oauth2.js index 4db44aa..9bea803 100644 --- a/src/oauth2.js +++ b/src/oauth2.js @@ -38,6 +38,7 @@ module.exports = function (RED) { this.name = config.name || ''; this.container = config.container || ''; this.access_token_url = config.access_token_url || ''; + this.redirect_uri = config.redirect_uri || ''; this.grant_type = config.grant_type || ''; this.username = config.username || ''; this.password = config.password || ''; @@ -57,6 +58,7 @@ module.exports = function (RED) { // Register the input handler this.on('input', this.onInput.bind(this)); + this.host = RED.settings.uiHost || 'localhost'; } /** @@ -66,19 +68,29 @@ module.exports = function (RED) { * @param {Function} done - Function to indicate processing is complete. */ async onInput(msg, send, done) { + // console.log('OAuth2Node received input:', msg); + const options = this.generateOptions(msg); // Generate request options + // console.log('Generated options:', options); + this.configureProxy(); // Configure proxy settings + // console.log('Configured proxy settings:', this.proxy); + delete msg.oauth2Request; // Remove oauth2Request from msg options.form = this.cleanForm(options.form); // Clean the form data try { + // console.log('Making POST request...'); const response = await this.makePostRequest(options); // Make the POST request + // console.log('Received response:', response); this.handleResponse(response, msg, send); // Handle the response } catch (error) { + // console.error('Error making POST request:', error); this.handleError(error, msg, send); // Handle any errors } done(); // Indicate that processing is complete + // console.log('Finished processing input.'); } /** @@ -93,31 +105,32 @@ module.exports = function (RED) { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' }; - + // Set options based on grant type - if (this.grant_type === 'set_by_credentials') { + if (msg.oauth2Request) { + const creds = msg.oauth2Request.credentials; form = { - grant_type: msg.oauth2Request.credentials.grant_type, - scope: msg.oauth2Request.credentials.scope, - resource: msg.oauth2Request.credentials.resource, - state: msg.oauth2Request.credentials.state + grant_type: creds.grant_type || this.grant_type, + scope: creds.scope || this.scope, + resource: creds.resource || this.resource, + state: creds.state || this.state }; - - if (msg.oauth2Request.credentials.grant_type === 'password') { - form.username = msg.oauth2Request.credentials.username; - form.password = msg.oauth2Request.credentials.password; - } else if (msg.oauth2Request.credentials.grant_type === 'refresh_token') { - form.refresh_token = msg.oauth2Request.credentials.refresh_token; + + if (creds.grant_type === 'password') { + form.username = creds.username || this.username; + form.password = creds.password || this.password; + } else if (creds.grant_type === 'refresh_token') { + form.refresh_token = creds.refresh_token; } - + if (this.client_credentials_in_body) { - form.client_id = msg.oauth2Request.credentials.client_id; - form.client_secret = msg.oauth2Request.credentials.client_secret; + form.client_id = creds.client_id || this.client_id; + form.client_secret = creds.client_secret || this.client_secret; } else { - headers.Authorization = 'Basic ' + Buffer.from(`${msg.oauth2Request.credentials.client_id}:${msg.oauth2Request.credentials.client_secret}`).toString('base64'); + headers.Authorization = 'Basic ' + Buffer.from(`${creds.client_id}:${creds.client_secret}`).toString('base64'); } - - url = msg.oauth2Request.access_token_url; + + url = msg.oauth2Request.access_token_url || this.access_token_url; } else { form = { grant_type: this.grant_type, @@ -125,7 +138,7 @@ module.exports = function (RED) { resource: this.resource, state: this.state }; - + if (this.grant_type === 'password') { form.username = this.username; form.password = this.password; @@ -133,10 +146,19 @@ module.exports = function (RED) { const credentials = RED.nodes.getCredentials(this.id); if (credentials) { form.code = credentials.code; - form.redirect_uri = credentials.redirectUri; + form.redirect_uri = this.redirect_uri; + } + } else if (this.grant_type === 'implicit_flow') { + const credentials = RED.nodes.getCredentials(this.id); + if (credentials) { + form.client_id = this.client_id; + form.client_secret = this.client_secret; + form.code = credentials.code; + form.grant_type = 'authorization_code'; + form.redirect_uri = this.redirect_uri; } } - + if (this.client_credentials_in_body) { form.client_id = this.client_id; form.client_secret = this.client_secret; @@ -144,7 +166,7 @@ module.exports = function (RED) { headers.Authorization = 'Basic ' + Buffer.from(`${this.client_id}:${this.client_secret}`).toString('base64'); } } - + return { method: 'POST', url: url, @@ -153,7 +175,7 @@ module.exports = function (RED) { form: form }; } - + /** * Configures proxy settings. */ @@ -200,11 +222,10 @@ module.exports = function (RED) { * @param {Function} send - Function to send messages. */ handleResponse(response, msg, send) { - msg[this.container] = response.data || {}; + msg.oauth2Response = response.data || {}; this.setStatus('green', `HTTP ${response.status}, ok`); send(msg); } - /** * Handles errors from the POST request. * @param {Object} error - The error object. @@ -214,11 +235,15 @@ module.exports = function (RED) { handleError(error, msg, send) { const status = error.response ? error.response.status : error.code; const message = error.response ? error.response.statusText : error.message; - msg[this.container] = error.response || {}; + msg.oauth2Error = error.response || { status, message }; this.setStatus('red', `HTTP ${status}, ${message}`); - if (this.sendErrorsToCatch) send(msg); + if (this.sendErrorsToCatch) send([null, msg]); + else { + this.error(message, msg); + send([null, msg]); + } } - + /** * Sets the status of the node. * @param {string} color - The color of the status indicator. @@ -232,20 +257,6 @@ module.exports = function (RED) { } } - // Register the OAuth2Node node type - RED.nodes.registerType('oauth2-credentials', OAuth2Node, { - credentials: { - displayName: { type: 'text' }, - clientId: { type: 'text' }, - clientSecret: { type: 'password' }, - accessToken: { type: 'password' }, - refreshToken: { type: 'password' }, - expireTime: { type: 'password' }, - code: { type: 'password' }, - proxy: { type: 'json' } - } - }); - /** * Endpoint to retrieve OAuth2 credentials based on a token. * @param {Object} req - The request object. @@ -268,91 +279,47 @@ module.exports = function (RED) { RED.httpAdmin.get('/oauth2/redirect', (req, res) => { if (req.query.code) { const [node_id] = req.query.state.split(':'); - const credentials = RED.nodes.getCredentials(node_id); - if (credentials) { - credentials.code = req.query.code; - RED.nodes.addCredentials(node_id, credentials); - res.send(` - - - - - -

Success! This page can be closed if it doesn't do so automatically.

- - - `); - } - } else { - res.send('oauth2.error.no-credentials'); - } - }); - - /** - * Endpoint to handle the OAuth2 authorization code flow. - * @param {Object} req - The request object. - * @param {Object} res - The response object. - */ - RED.httpAdmin.get('/oauth2/auth', async (req, res) => { - if (!req.query.clientId || !req.query.clientSecret || !req.query.id || !req.query.callback) { - res.sendStatus(400); - return; - } + let credentials = RED.nodes.getCredentials(node_id); - const { clientId, clientSecret, id: node_id, callback, redirectUri, authorizationEndpoint, scope, resource } = req.query; - const csrfToken = crypto.randomBytes(18).toString('base64').replace(/\//g, '-').replace(/\+/g, '_'); - const credentials = { clientId, clientSecret, callback, redirectUri, csrfToken }; - - const proxy = RED.nodes.getNode(req.query.proxy); - const proxyOptions = proxy ? new URL(proxy.url) : null; - - res.cookie('csrf', csrfToken); - - const redirectUrl = new URL(authorizationEndpoint); - redirectUrl.searchParams.set('client_id', clientId); - redirectUrl.searchParams.set('redirect_uri', redirectUri); - redirectUrl.searchParams.set('state', `${node_id}:${csrfToken}`); - redirectUrl.searchParams.set('scope', scope); - redirectUrl.searchParams.set('resource', resource); - redirectUrl.searchParams.set('response_type', 'code'); + if (!credentials) { + credentials = {}; + } - try { - const response = await axios.get(redirectUrl.toString(), { - httpsAgent: new https.Agent({ rejectUnauthorized: false }), - httpAgent: new http.Agent({ rejectUnauthorized: false }), - proxy: proxyOptions - }); - res.redirect(response.request.res.responseUrl); + credentials = { ...credentials, ...req.query }; RED.nodes.addCredentials(node_id, credentials); - } catch (error) { - res.sendStatus(404); - } - }); - /** - * Endpoint to handle the OAuth2 authorization callback. - * @param {Object} req - The request object. - * @param {Object} res - The response object. - */ - RED.httpAdmin.get('/oauth2/auth/callback', (req, res) => { - if (req.query.error) { - return res.send(`oauth2.error.error: ${req.query.error}, description: ${req.query.error_description}`); - } - const [node_id, csrfToken] = req.query.state.split(':'); - const credentials = RED.nodes.getCredentials(node_id); - if (!credentials || !credentials.clientId || !credentials.clientSecret || csrfToken !== credentials.csrfToken) { - return res.status(401).send('oauth2.error.token-mismatch'); + res.send(` + + + + + +

Success! This page can be closed if it doesn't do so automatically.

+ + + `); + } else { + res.send('oauth2.error.no-credentials'); } }); // Register the OAuth2Node node type - RED.nodes.registerType('oauth2', OAuth2Node); + RED.nodes.registerType('oauth2', OAuth2Node, { + credentials: { + clientId: { type: 'text' }, + clientSecret: { type: 'password' }, + accessToken: { type: 'password' }, + refreshToken: { type: 'password' }, + expireTime: { type: 'password' }, + code: { type: 'password' } + } + }); }; diff --git a/test/oauth2_spec.js b/test/oauth2_spec.js new file mode 100644 index 0000000..8d40067 --- /dev/null +++ b/test/oauth2_spec.js @@ -0,0 +1,139 @@ +/* eslint-disable no-unused-vars */ +const should = require('should'); // eslint-disable-line no-unused-vars +/* eslint-enable no-unused-vars */const helper = require('node-red-node-test-helper'); +const nock = require('nock'); +const oauth2Node = require('../src/oauth2.js'); + +helper.init(require.resolve('node-red')); + +describe('OAuth2 Node', function () { + this.timeout(10000); // Increase timeout to 10000ms for more room + + before(function(done) { + console.log('Starting Node-RED server...'); + helper.startServer(done); + }); + + after(function(done) { + console.log('Stopping Node-RED server...'); + helper.stopServer(done); + }); + + afterEach(function(done) { + console.log('Unloading flows...'); + helper.unload().then(function() { + done(); + }); + }); + + it('should be loaded', function(done) { + console.log('Testing if node loads correctly...'); + const flow = [{ id: 'n1', type: 'oauth2', name: 'oauth2' }]; + helper.load(oauth2Node, flow, function() { + const n1 = helper.getNode('n1'); + try { + n1.should.have.property('name', 'oauth2'); + console.log('Node loaded successfully'); + done(); + } catch (err) { + console.error('Node failed to load', err); + done(err); + } + }); + }); + + it('should handle input and make POST request', function(done) { + console.log('Testing input handling and POST request...'); + const flow = [ + { id: 'n1', type: 'oauth2', name: 'oauth2', wires: [['n2']] }, + { id: 'n2', type: 'helper' } + ]; + const credentials = { + clientId: 'testClientId', + clientSecret: 'testClientSecret' + }; + + helper.load(oauth2Node, flow, credentials, function() { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + + console.log('Setting up nock for example.com...'); + const scope = nock('https://example.com') + .post('/oauth2/token') + .reply(200, { access_token: 'mocked_access_token' }); + + n2.on('input', function(msg) { + console.log('Received input on helper node'); + try { + msg.should.have.property('oauth2Response'); + msg.oauth2Response.should.have.property('access_token', 'mocked_access_token'); + scope.done(); // Verify if the nock interceptor was called + done(); + } catch (err) { + console.error('Failed input handling test', err); + done(err); + } + }); + + console.log('Sending input to node...'); + n1.receive({ + oauth2Request: { + access_token_url: 'https://example.com/oauth2/token', + credentials: { + grant_type: 'client_credentials', + client_id: 'testClientId', + client_secret: 'testClientSecret', + scope: 'testScope' + } + } + }); + }); + }); + + it('should handle errors', function(done) { + console.log('Testing error handling...'); + const flow = [ + { id: 'n1', type: 'oauth2', name: 'oauth2', wires: [[], ['n3']] }, + { id: 'n3', type: 'helper' } + ]; + const credentials = { + clientId: 'testClientId', + clientSecret: 'testClientSecret' + }; + + helper.load(oauth2Node, flow, credentials, function() { + const n1 = helper.getNode('n1'); + const n3 = helper.getNode('n3'); + + console.log('Setting up nock for invalid-url.com...'); + const scope = nock('https://invalid-url.com') + .post('/') + .replyWithError('mocked error'); + + n3.on('input', function(msg) { + console.log('Received input on error helper node'); + try { + msg.should.have.property('oauth2Error'); + scope.done(); // Verify if the nock interceptor was called + done(); + } catch (err) { + console.error('Failed error handling test', err); + done(err); + } + }); + + console.log('Sending input to node...'); + n1.receive({ + oauth2Request: { + access_token_url: 'https://invalid-url.com', + credentials: { + grant_type: 'client_credentials', + client_id: 'testClientId', + client_secret: 'testClientSecret', + scope: 'testScope' + } + } + }); + }); + }); +});