From 764582e177de143494bddaed2cbddaf2e42c35f9 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Tue, 24 Oct 2023 00:08:55 +0300 Subject: [PATCH 01/50] improve stackexchange auth testing Change auth tests to include all shields of the base class. The code is formated to be used in more general cases and increases code reuseability. --- .../stackexchange/stackexchange-base.spec.js | 67 +++++++++++++++---- 1 file changed, 53 insertions(+), 14 deletions(-) diff --git a/services/stackexchange/stackexchange-base.spec.js b/services/stackexchange/stackexchange-base.spec.js index 771cf7be32470..9f68591124373 100644 --- a/services/stackexchange/stackexchange-base.spec.js +++ b/services/stackexchange/stackexchange-base.spec.js @@ -1,12 +1,27 @@ import Joi from 'joi' import { expect } from 'chai' import nock from 'nock' +import { pathParams } from '../index.js' import { cleanUpNockAfterEach, defaultContext } from '../test-helpers.js' import { StackExchangeBase } from './stackexchange-base.js' +import StackExchangeMonthlyQuestions from './stackexchange-monthlyquestions.service.js' +import StackExchangeReputation from './stackexchange-reputation.service.js' +import StackExchangeQuestions from './stackexchange-taginfo.service.js' class DummyStackExchangeService extends StackExchangeBase { static route = { base: 'fake-base' } + static openApi = { + '/fake-base': { + get: { + parameters: pathParams({ + name: 'fakeparam', + example: 'fakeparam', + }), + }, + }, + } + async handle() { const data = await this.fetch({ schema: Joi.any(), @@ -16,23 +31,47 @@ class DummyStackExchangeService extends StackExchangeBase { } } -describe('StackExchangeBase', function () { - describe('auth', function () { - cleanUpNockAfterEach() +// format is [class, example response from server] +const testClasses = [ + [DummyStackExchangeService, { message: 'fake message' }], + [StackExchangeMonthlyQuestions, { total: 8 }], + [StackExchangeReputation, { items: [{ reputation: 8 }] }], + [StackExchangeQuestions, { items: [{ count: 8 }] }], +] - const config = { private: { stackapps_api_key: 'fake-key' } } +for (const [serviceClass, dummyResponse] of testClasses) { + testAuth(serviceClass, dummyResponse) +} - it('sends the auth information as configured', async function () { - const scope = nock('https://api.stackexchange.com') - .get('/2.2/tags/python/info') - .query({ key: 'fake-key' }) - .reply(200, { message: 'fake message' }) +function testAuth(serviceClass, dummyResponse) { + describe(serviceClass.name, function () { + describe('auth', function () { + cleanUpNockAfterEach() - expect( - await DummyStackExchangeService.invoke(defaultContext, config, {}), - ).to.deep.equal({ message: 'fake message' }) + const config = { private: { stackapps_api_key: 'fake-key' } } + const firstOpenapiPath = Object.keys(serviceClass.openApi)[0] + const exampleInvokeParams = serviceClass.openApi[ + firstOpenapiPath + ].get.parameters.reduce((acc, obj) => { + acc[obj.name] = obj.example + return acc + }, {}) - scope.done() + it('sends the auth information as configured', async function () { + const scope = nock('https://api.stackexchange.com') + .get(/.*/) + .query(queryObject => queryObject.key === 'fake-key') + .reply(200, dummyResponse) + expect( + await serviceClass.invoke( + defaultContext, + config, + exampleInvokeParams, + ), + ).to.not.have.property('isError') + + scope.done() + }) }) }) -}) +} From 9dd597dcf2ab772077b23e9b9918114c88f5384d Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Tue, 16 Jan 2024 23:57:04 +0200 Subject: [PATCH 02/50] Remove dummy auth test We already test all existing classes, no need for a dummy --- .../stackexchange/stackexchange-base.spec.js | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/services/stackexchange/stackexchange-base.spec.js b/services/stackexchange/stackexchange-base.spec.js index 9f68591124373..ec8a4e14915a3 100644 --- a/services/stackexchange/stackexchange-base.spec.js +++ b/services/stackexchange/stackexchange-base.spec.js @@ -1,39 +1,12 @@ -import Joi from 'joi' import { expect } from 'chai' import nock from 'nock' -import { pathParams } from '../index.js' import { cleanUpNockAfterEach, defaultContext } from '../test-helpers.js' -import { StackExchangeBase } from './stackexchange-base.js' import StackExchangeMonthlyQuestions from './stackexchange-monthlyquestions.service.js' import StackExchangeReputation from './stackexchange-reputation.service.js' import StackExchangeQuestions from './stackexchange-taginfo.service.js' -class DummyStackExchangeService extends StackExchangeBase { - static route = { base: 'fake-base' } - - static openApi = { - '/fake-base': { - get: { - parameters: pathParams({ - name: 'fakeparam', - example: 'fakeparam', - }), - }, - }, - } - - async handle() { - const data = await this.fetch({ - schema: Joi.any(), - url: 'https://api.stackexchange.com/2.2/tags/python/info', - }) - return { message: data.message } - } -} - // format is [class, example response from server] const testClasses = [ - [DummyStackExchangeService, { message: 'fake message' }], [StackExchangeMonthlyQuestions, { total: 8 }], [StackExchangeReputation, { items: [{ reputation: 8 }] }], [StackExchangeQuestions, { items: [{ count: 8 }] }], From 7bc3cc069acb094fa042d5ca3578c5a90747fdd9 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sat, 20 Jan 2024 14:14:11 +0200 Subject: [PATCH 03/50] Add getBadgeExampleCall to test-helpers Add getBadgeExampleCall to extract the first OpenAPI example then reformat it for service invoke function. --- services/test-helpers.js | 49 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/services/test-helpers.js b/services/test-helpers.js index 8fb099ba05b3a..2cfa879fa17f8 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -1,6 +1,7 @@ import nock from 'nock' import config from 'config' import { fetch } from '../core/base-service/got.js' +import BaseService from '../core/base-service/base.js' const runnerConfig = config.util.toObject() function cleanUpNockAfterEach() { @@ -30,6 +31,52 @@ function noToken(serviceClass) { } } +/** + * Retrieves an example set of parameters for invoking a service class using OpenAPI example of that class. + * + * @param {BaseService} serviceClass The service class containing OpenAPI specifications. + * @returns {object} An object with call params to use with a service invoke of the first OpenAPI example. + * @throws {TypeError} - Throws a TypeError if the input `serviceClass` is not an instance of BaseService, + * or if it lacks the expected structure. + * + * @example + * // Example usage: + * const example = getBadgeExampleCall(StackExchangeReputation) + * console.log(example) + * // Output: { stackexchangesite: 'stackoverflow', query: '123' } + * StackExchangeReputation.invoke(defaultContext, config, example) + */ +function getBadgeExampleCall(serviceClass) { + if (!(serviceClass.prototype instanceof BaseService)) { + throw new TypeError( + 'Invalid serviceClass: Must be an instance of BaseService.', + ) + } + + const firstOpenapiPath = Object.keys(serviceClass.openApi)[0] + if (!firstOpenapiPath) { + throw new TypeError( + `Missing OpenAPI in service class ${serviceClass.constructor.name}.`, + ) + } + + const firstOpenapiExampleParams = + serviceClass.openApi[firstOpenapiPath].get.parameters + if (!Array.isArray(firstOpenapiExampleParams)) { + throw new TypeError( + `Missing or invalid OpenAPI examples in ${serviceClass.constructor.name}.`, + ) + } + + // reformat structure for serviceClass.invoke + const exampleInvokeParams = firstOpenapiExampleParams.reduce((acc, obj) => { + acc[obj.name] = obj.example + return acc + }, {}) + + return exampleInvokeParams +} + const defaultContext = { requestFetcher: fetch } -export { cleanUpNockAfterEach, noToken, defaultContext } +export { cleanUpNockAfterEach, noToken, getBadgeExampleCall, defaultContext } From f6da3afb3e6b4d1eb5266bfa7d4f457fe8633156 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sat, 20 Jan 2024 14:15:01 +0200 Subject: [PATCH 04/50] Use getBadgeExampleCall in stackexchange-base tests --- services/stackexchange/stackexchange-base.spec.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/services/stackexchange/stackexchange-base.spec.js b/services/stackexchange/stackexchange-base.spec.js index ec8a4e14915a3..20ace4b4147e4 100644 --- a/services/stackexchange/stackexchange-base.spec.js +++ b/services/stackexchange/stackexchange-base.spec.js @@ -1,6 +1,10 @@ import { expect } from 'chai' import nock from 'nock' -import { cleanUpNockAfterEach, defaultContext } from '../test-helpers.js' +import { + cleanUpNockAfterEach, + defaultContext, + getBadgeExampleCall, +} from '../test-helpers.js' import StackExchangeMonthlyQuestions from './stackexchange-monthlyquestions.service.js' import StackExchangeReputation from './stackexchange-reputation.service.js' import StackExchangeQuestions from './stackexchange-taginfo.service.js' @@ -22,13 +26,7 @@ function testAuth(serviceClass, dummyResponse) { cleanUpNockAfterEach() const config = { private: { stackapps_api_key: 'fake-key' } } - const firstOpenapiPath = Object.keys(serviceClass.openApi)[0] - const exampleInvokeParams = serviceClass.openApi[ - firstOpenapiPath - ].get.parameters.reduce((acc, obj) => { - acc[obj.name] = obj.example - return acc - }, {}) + const exampleInvokeParams = getBadgeExampleCall(serviceClass) it('sends the auth information as configured', async function () { const scope = nock('https://api.stackexchange.com') From 31c3f949bfdb638d6bf250239a20ad10432bc177 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sun, 21 Jan 2024 00:42:33 +0200 Subject: [PATCH 05/50] Fix getBadgeExampleCall Errors --- services/test-helpers.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/services/test-helpers.js b/services/test-helpers.js index 2cfa879fa17f8..5e1214d433b43 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -53,18 +53,19 @@ function getBadgeExampleCall(serviceClass) { ) } - const firstOpenapiPath = Object.keys(serviceClass.openApi)[0] - if (!firstOpenapiPath) { + if (!serviceClass.openApi) { throw new TypeError( - `Missing OpenAPI in service class ${serviceClass.constructor.name}.`, + `Missing OpenAPI in service class ${serviceClass.name}.`, ) } + const firstOpenapiPath = Object.keys(serviceClass.openApi)[0] + const firstOpenapiExampleParams = serviceClass.openApi[firstOpenapiPath].get.parameters if (!Array.isArray(firstOpenapiExampleParams)) { throw new TypeError( - `Missing or invalid OpenAPI examples in ${serviceClass.constructor.name}.`, + `Missing or invalid OpenAPI examples in ${serviceClass.name}.`, ) } From 18ec38723a38bdbeca1edd7b6e6d057a361375ac Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sun, 21 Jan 2024 00:59:43 +0200 Subject: [PATCH 06/50] Add testAuth to test-helpers Add the testAuth function which tests auth of a service (badge) using a provided dummy response. --- services/test-helpers.js | 49 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/services/test-helpers.js b/services/test-helpers.js index 5e1214d433b43..80d5c54cbdc4a 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -1,3 +1,4 @@ +import { expect } from 'chai' import nock from 'nock' import config from 'config' import { fetch } from '../core/base-service/got.js' @@ -78,6 +79,52 @@ function getBadgeExampleCall(serviceClass) { return exampleInvokeParams } +/** + * Test authentication of a badge for it's first OpenAPI example using a provided dummyResponse + * + * @param {BaseService} serviceClass The service class tested. + * @param {object} dummyResponse An object containing the dummy response by the server. + * @throws {TypeError} - Throws a TypeError if the input `serviceClass` is not an instance of BaseService, + * or if `serviceClass` is missing authorizedOrigins. + * + * @example + * // Example usage: + * testAuth(StackExchangeReputation, { items: [{ reputation: 8 }] }) + */ +async function testAuth(serviceClass, dummyResponse) { + if (!(serviceClass.prototype instanceof BaseService)) { + throw new TypeError( + 'Invalid serviceClass: Must be an instance of BaseService.', + ) + } + + cleanUpNockAfterEach() + + const config = { private: { stackapps_api_key: 'fake-key' } } + const exampleInvokeParams = getBadgeExampleCall(serviceClass) + const authOrigin = serviceClass.auth.authorizedOrigins[0] + + if (!authOrigin) { + throw new TypeError(`Missing authorizedOrigins for ${serviceClass.name}.`) + } + + const scope = nock(authOrigin) + .get(/.*/) + .query(queryObject => queryObject.key === 'fake-key') + .reply(200, dummyResponse) + expect( + await serviceClass.invoke(defaultContext, config, exampleInvokeParams), + ).to.not.have.property('isError') + + scope.done() +} + const defaultContext = { requestFetcher: fetch } -export { cleanUpNockAfterEach, noToken, getBadgeExampleCall, defaultContext } +export { + cleanUpNockAfterEach, + noToken, + getBadgeExampleCall, + testAuth, + defaultContext, +} From 1688e588a0be43bcfe78efb991726b0e9a5635d4 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sun, 21 Jan 2024 01:01:07 +0200 Subject: [PATCH 07/50] Refactor stackexchange-base.spec.js to use testAuth from test-helpers --- .../stackexchange/stackexchange-base.spec.js | 33 ++----------------- 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/services/stackexchange/stackexchange-base.spec.js b/services/stackexchange/stackexchange-base.spec.js index 20ace4b4147e4..bb8f22d519be9 100644 --- a/services/stackexchange/stackexchange-base.spec.js +++ b/services/stackexchange/stackexchange-base.spec.js @@ -1,10 +1,4 @@ -import { expect } from 'chai' -import nock from 'nock' -import { - cleanUpNockAfterEach, - defaultContext, - getBadgeExampleCall, -} from '../test-helpers.js' +import { testAuth } from '../test-helpers.js' import StackExchangeMonthlyQuestions from './stackexchange-monthlyquestions.service.js' import StackExchangeReputation from './stackexchange-reputation.service.js' import StackExchangeQuestions from './stackexchange-taginfo.service.js' @@ -17,31 +11,10 @@ const testClasses = [ ] for (const [serviceClass, dummyResponse] of testClasses) { - testAuth(serviceClass, dummyResponse) -} - -function testAuth(serviceClass, dummyResponse) { - describe(serviceClass.name, function () { + describe(`${serviceClass.name}`, function () { describe('auth', function () { - cleanUpNockAfterEach() - - const config = { private: { stackapps_api_key: 'fake-key' } } - const exampleInvokeParams = getBadgeExampleCall(serviceClass) - it('sends the auth information as configured', async function () { - const scope = nock('https://api.stackexchange.com') - .get(/.*/) - .query(queryObject => queryObject.key === 'fake-key') - .reply(200, dummyResponse) - expect( - await serviceClass.invoke( - defaultContext, - config, - exampleInvokeParams, - ), - ).to.not.have.property('isError') - - scope.done() + return testAuth(serviceClass, dummyResponse) }) }) }) From 609c01728f34c036d44f3bb9600918ed5a3c6973 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sun, 21 Jan 2024 01:13:53 +0200 Subject: [PATCH 08/50] Split stackexchange-base.spec into per service test file --- .../stackexchange/stackexchange-base.spec.js | 21 ------------------- .../stackexchange-monthlyquestions.spec.js | 10 +++++++++ .../stackexchange-reputation.spec.js | 10 +++++++++ .../stackexchange-taginfo.spec.js | 10 +++++++++ 4 files changed, 30 insertions(+), 21 deletions(-) delete mode 100644 services/stackexchange/stackexchange-base.spec.js create mode 100644 services/stackexchange/stackexchange-monthlyquestions.spec.js create mode 100644 services/stackexchange/stackexchange-reputation.spec.js create mode 100644 services/stackexchange/stackexchange-taginfo.spec.js diff --git a/services/stackexchange/stackexchange-base.spec.js b/services/stackexchange/stackexchange-base.spec.js deleted file mode 100644 index bb8f22d519be9..0000000000000 --- a/services/stackexchange/stackexchange-base.spec.js +++ /dev/null @@ -1,21 +0,0 @@ -import { testAuth } from '../test-helpers.js' -import StackExchangeMonthlyQuestions from './stackexchange-monthlyquestions.service.js' -import StackExchangeReputation from './stackexchange-reputation.service.js' -import StackExchangeQuestions from './stackexchange-taginfo.service.js' - -// format is [class, example response from server] -const testClasses = [ - [StackExchangeMonthlyQuestions, { total: 8 }], - [StackExchangeReputation, { items: [{ reputation: 8 }] }], - [StackExchangeQuestions, { items: [{ count: 8 }] }], -] - -for (const [serviceClass, dummyResponse] of testClasses) { - describe(`${serviceClass.name}`, function () { - describe('auth', function () { - it('sends the auth information as configured', async function () { - return testAuth(serviceClass, dummyResponse) - }) - }) - }) -} diff --git a/services/stackexchange/stackexchange-monthlyquestions.spec.js b/services/stackexchange/stackexchange-monthlyquestions.spec.js new file mode 100644 index 0000000000000..45f4dadf8b208 --- /dev/null +++ b/services/stackexchange/stackexchange-monthlyquestions.spec.js @@ -0,0 +1,10 @@ +import { testAuth } from '../test-helpers.js' +import StackExchangeMonthlyQuestions from './stackexchange-monthlyquestions.service.js' + +describe('StackExchangeMonthlyQuestions', function () { + describe('auth', function () { + it('sends the auth information as configured', async function () { + return testAuth(StackExchangeMonthlyQuestions, { total: 8 }) + }) + }) +}) diff --git a/services/stackexchange/stackexchange-reputation.spec.js b/services/stackexchange/stackexchange-reputation.spec.js new file mode 100644 index 0000000000000..5b1625e6d7d23 --- /dev/null +++ b/services/stackexchange/stackexchange-reputation.spec.js @@ -0,0 +1,10 @@ +import { testAuth } from '../test-helpers.js' +import StackExchangeReputation from './stackexchange-reputation.service.js' + +describe('StackExchangeReputation', function () { + describe('auth', function () { + it('sends the auth information as configured', async function () { + return testAuth(StackExchangeReputation, { items: [{ reputation: 8 }] }) + }) + }) +}) diff --git a/services/stackexchange/stackexchange-taginfo.spec.js b/services/stackexchange/stackexchange-taginfo.spec.js new file mode 100644 index 0000000000000..b53b8c968ab76 --- /dev/null +++ b/services/stackexchange/stackexchange-taginfo.spec.js @@ -0,0 +1,10 @@ +import { testAuth } from '../test-helpers.js' +import StackExchangeQuestions from './stackexchange-taginfo.service.js' + +describe('StackExchangeQuestions', function () { + describe('auth', function () { + it('sends the auth information as configured', async function () { + return testAuth(StackExchangeQuestions, { items: [{ count: 8 }] }) + }) + }) +}) From ffc780091071cf4dc1cc2a445bcedaa63c8a1be8 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sun, 11 Feb 2024 00:25:23 +0200 Subject: [PATCH 09/50] Add all auth methods to testAuth Add all auth methods used to testAuth to be generic and used by all services. Add helper functions to make testAuth more readable --- services/test-helpers.js | 155 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 151 insertions(+), 4 deletions(-) diff --git a/services/test-helpers.js b/services/test-helpers.js index 80d5c54cbdc4a..1511fbca6231f 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -1,3 +1,4 @@ +import dayjs from 'dayjs' import { expect } from 'chai' import nock from 'nock' import config from 'config' @@ -79,6 +80,110 @@ function getBadgeExampleCall(serviceClass) { return exampleInvokeParams } +/** + * Generates a configuration object with a fake key based on the provided class. + * For use in auth tests where a config with a test key is required. + * + * @param {BaseService} serviceClass - The class to generate configuration for. + * @param {string} fakeKey - The fake key to be used in the configuration. + * @param {string} fakeUser - Optional, The fake user to be used in the configuration. + * @returns {object} - The configuration object. + * @throws {TypeError} - Throws an error if the input is not a class. + */ +function generateFakeConfig(serviceClass, fakeKey, fakeUser) { + if ( + !serviceClass || + !serviceClass.prototype || + !(serviceClass.prototype instanceof BaseService) + ) { + throw new TypeError( + 'Invalid serviceClass: Must be an instance of BaseService.', + ) + } + if (!fakeKey || typeof fakeKey !== 'string') { + throw new TypeError('Invalid fakeKey: Must be a String.') + } + + if (!serviceClass.auth) { + throw new Error(`Missing auth for ${serviceClass.name}.`) + } + if (!serviceClass.auth.passKey) { + throw new Error(`Missing auth.passKey for ${serviceClass.name}.`) + } + // Extract the passKey property from auth, or use a default if not present + const passKeyProperty = serviceClass.auth.passKey + let passUserProperty = 'placeholder' + if (fakeUser) { + if (typeof fakeKey !== 'string') { + throw new TypeError('Invalid fakeUser: Must be a String.') + } + if (!serviceClass.auth.userKey) { + throw new Error(`Missing auth.userKey for ${serviceClass.name}.`) + } + passUserProperty = serviceClass.auth.userKey + } + + // Build and return the configuration object with the fake key + return { + private: { + [passKeyProperty]: fakeKey, + [passUserProperty]: fakeUser, + }, + } +} + +/** + * Returns the name of the auth method of the service class provided + * + * @param {BaseService} serviceClass The service class to extract auth method from. + * @throws {TypeError} - Throws a TypeError if the input `serviceClass` is not an instance of BaseService + * or if the authHelper function is missing in fetch. + * @returns {string} The auth method of the service class provided. + * May return on of the following strings: BasicAuth, ApiKeyHeader, BearerAuthHeader, QueryStringAuth, JwtAuth. + * + * @example + * // Example usage: + * getAuthMethod(SonarCoverage) + * // outputs "BasicAuth" + */ +function getAuthMethod(serviceClass) { + if ( + !serviceClass || + !serviceClass.prototype || + !(serviceClass.prototype instanceof BaseService) + ) { + throw new TypeError( + `Invalid serviceClass ${serviceClass}: Must be an instance of BaseService.`, + ) + } + const fetchFunctionString = serviceClass.prototype.fetch.toString() + const result = fetchFunctionString.match( + /this\.authHelper\.with([A-Z][a-zA-Z0-9_]+)\(/, + ) + if (result) { + return result[1] + } else { + throw new TypeError( + 'Invalid serviceClass: Missing authHelper function in fetch.', + ) + } +} + +/** + * Generate a fake JWT Token valid for 1 hour for use in testing. + * + * @returns {string} Fake JWT Token valid for 1 hour. + */ +function fakeJwtToken() { + const fakeJwtPayload = { exp: dayjs().add(1, 'hours').unix() } + const fakeJwtPayloadJsonString = JSON.stringify(fakeJwtPayload) + const fakeJwtPayloadBase64 = Buffer.from(fakeJwtPayloadJsonString).toString( + 'base64', + ) + const jwtToken = `FakeHeader.${fakeJwtPayloadBase64}.fakeSignature` + return jwtToken +} + /** * Test authentication of a badge for it's first OpenAPI example using a provided dummyResponse * @@ -100,7 +205,10 @@ async function testAuth(serviceClass, dummyResponse) { cleanUpNockAfterEach() - const config = { private: { stackapps_api_key: 'fake-key' } } + const fakeUser = 'fake-user' + const fakeSecret = 'fake-secret' + const authMethod = getAuthMethod(serviceClass) + const config = generateFakeConfig(serviceClass, fakeSecret) const exampleInvokeParams = getBadgeExampleCall(serviceClass) const authOrigin = serviceClass.auth.authorizedOrigins[0] @@ -109,9 +217,48 @@ async function testAuth(serviceClass, dummyResponse) { } const scope = nock(authOrigin) - .get(/.*/) - .query(queryObject => queryObject.key === 'fake-key') - .reply(200, dummyResponse) + switch (authMethod) { + case 'BasicAuth': + scope + .get(/.*/) + .basicAuth({ fakeUser, fakeSecret }) + .reply(200, dummyResponse) + break + case 'ApiKeyHeader': + // TODO may fail if header is not default (see auth-helper.js - withApiKeyHeader) + scope + .get(/.*/) + .matchHeader('x-api-key', fakeSecret) + .reply(200, dummyResponse) + break + case 'BearerAuthHeader': + scope + .get(/.*/) + .matchHeader('Authorization', `Bearer ${fakeSecret}`) + .reply(200, dummyResponse) + break + case 'QueryStringAuth': + scope + .get(/.*/) + .query(queryObject => queryObject.key === fakeSecret) + .reply(200, dummyResponse) + break + case 'JwtAuth': { + const fakeToken = fakeJwtToken() + scope + .post(/.*/, { username: fakeUser, password: fakeSecret }) + .reply(200, fakeToken) + scope + .get(/.*/) + .matchHeader('Authorization', `Bearer ${fakeToken}`) + .reply(200, dummyResponse) + break + } + + default: + throw new TypeError(`Unkown auth method for ${serviceClass.name}.`) + } + expect( await serviceClass.invoke(defaultContext, config, exampleInvokeParams), ).to.not.have.property('isError') From 3e5c98d0822accd6e1fb912a6d64a74e30d7b324 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Mon, 12 Feb 2024 00:01:21 +0200 Subject: [PATCH 10/50] Handle non-default bearer and api headers --- services/test-helpers.js | 73 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/services/test-helpers.js b/services/test-helpers.js index 1511fbca6231f..07061b2091322 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -169,6 +169,72 @@ function getAuthMethod(serviceClass) { } } +/** + * Returns the prefix of the bearer token for a service class. + * + * @param {BaseService} serviceClass The service class to extract auth method from. + * @throws {TypeError} - Throws a TypeError if the input `serviceClass` is not an instance of BaseService. + * @returns {string} Bearer token prefix. + * + * @example + * // Example usage: + * getBearerPrefixOfService(Discord) + * // outputs 'Bot' + */ +function getBearerPrefixOfService(serviceClass) { + if ( + !serviceClass || + !serviceClass.prototype || + !(serviceClass.prototype instanceof BaseService) + ) { + throw new TypeError( + `Invalid serviceClass ${serviceClass}: Must be an instance of BaseService.`, + ) + } + const fetchFunctionString = serviceClass.prototype.fetch.toString() + const result = fetchFunctionString.match( + /withBearerAuthHeader\([\s\S]*,\s+['"`]([\s\S]*)['"`],?\s*\)/, + ) + if (result) { + return result[1] + } else { + return 'Bearer' + } +} + +/** + * Returns the prefix of the bearer token for a service class. + * + * @param {BaseService} serviceClass The service class to extract auth method from. + * @throws {TypeError} - Throws a TypeError if the input `serviceClass` is not an instance of BaseService. + * @returns {string} Bearer token prefix. + * + * @example + * // Example usage: + * getApiHeaderKeyOfService(CurseForgeDownloads) + * // outputs 'x-api-key' + */ +function getApiHeaderKeyOfService(serviceClass) { + if ( + !serviceClass || + !serviceClass.prototype || + !(serviceClass.prototype instanceof BaseService) + ) { + throw new TypeError( + `Invalid serviceClass ${serviceClass}: Must be an instance of BaseService.`, + ) + } + const fetchFunctionString = serviceClass.prototype.fetch.toString() + const result = fetchFunctionString.match( + /withApiKeyHeader\([\s\S]*,\s+['"`]([\s\S]*)['"`],?\s*\)/, + ) + if (result) { + return result[1] + } else { + return 'x-api-key' + } +} + /** * Generate a fake JWT Token valid for 1 hour for use in testing. * @@ -228,13 +294,16 @@ async function testAuth(serviceClass, dummyResponse) { // TODO may fail if header is not default (see auth-helper.js - withApiKeyHeader) scope .get(/.*/) - .matchHeader('x-api-key', fakeSecret) + .matchHeader(getApiHeaderKeyOfService(serviceClass), fakeSecret) .reply(200, dummyResponse) break case 'BearerAuthHeader': scope .get(/.*/) - .matchHeader('Authorization', `Bearer ${fakeSecret}`) + .matchHeader( + 'Authorization', + `${getBearerPrefixOfService(serviceClass)} ${fakeSecret}`, + ) .reply(200, dummyResponse) break case 'QueryStringAuth': From 876708f1e74bd76ea573478fc05ab1ac18d2ac73 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Mon, 12 Feb 2024 00:01:58 +0200 Subject: [PATCH 11/50] Add discord.spec.js as first attempt for bearer auth --- services/discord/discord.spec.js | 36 ++++---------------------------- 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/services/discord/discord.spec.js b/services/discord/discord.spec.js index c563ea826d423..328ea0b9e0477 100644 --- a/services/discord/discord.spec.js +++ b/services/discord/discord.spec.js @@ -1,38 +1,10 @@ -import { expect } from 'chai' -import nock from 'nock' -import { cleanUpNockAfterEach, defaultContext } from '../test-helpers.js' +import { testAuth } from '../test-helpers.js' import Discord from './discord.service.js' describe('Discord', function () { - cleanUpNockAfterEach() - - it('sends the auth information as configured', async function () { - const pass = 'password' - const config = { - private: { - discord_bot_token: pass, - }, - } - - const scope = nock('https://discord.com', { - // This ensures that the expected credential is actually being sent with the HTTP request. - // Without this the request wouldn't match and the test would fail. - reqheaders: { Authorization: 'Bot password' }, + describe('auth', function () { + it('sends the auth information as configured', async function () { + return testAuth(Discord, { presence_count: 125 }) }) - .get('/api/v6/guilds/12345/widget.json') - .reply(200, { - presence_count: 125, - }) - - expect( - await Discord.invoke(defaultContext, config, { - serverId: '12345', - }), - ).to.deep.equal({ - message: '125 online', - color: 'brightgreen', - }) - - scope.done() }) }) From c41f60fa9f705c6a9a9a3120387ec75ad2fd07d8 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Mon, 12 Feb 2024 01:49:01 +0200 Subject: [PATCH 12/50] Fix basic auth user --- services/test-helpers.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/test-helpers.js b/services/test-helpers.js index 07061b2091322..5209682a77474 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -271,10 +271,10 @@ async function testAuth(serviceClass, dummyResponse) { cleanUpNockAfterEach() - const fakeUser = 'fake-user' + const fakeUser = serviceClass.auth.userKey ? 'fake-user' : undefined const fakeSecret = 'fake-secret' const authMethod = getAuthMethod(serviceClass) - const config = generateFakeConfig(serviceClass, fakeSecret) + const config = generateFakeConfig(serviceClass, fakeSecret, fakeUser) const exampleInvokeParams = getBadgeExampleCall(serviceClass) const authOrigin = serviceClass.auth.authorizedOrigins[0] @@ -287,7 +287,7 @@ async function testAuth(serviceClass, dummyResponse) { case 'BasicAuth': scope .get(/.*/) - .basicAuth({ fakeUser, fakeSecret }) + .basicAuth({ user: fakeUser, pass: fakeSecret }) .reply(200, dummyResponse) break case 'ApiKeyHeader': From f4cc1afbea86dd06ac49f9c3d8275ad95652d6e7 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Mon, 12 Feb 2024 01:53:50 +0200 Subject: [PATCH 13/50] Add dynamic authorizedOrigins --- services/test-helpers.js | 57 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/services/test-helpers.js b/services/test-helpers.js index 5209682a77474..25a44a07cb825 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -87,10 +87,16 @@ function getBadgeExampleCall(serviceClass) { * @param {BaseService} serviceClass - The class to generate configuration for. * @param {string} fakeKey - The fake key to be used in the configuration. * @param {string} fakeUser - Optional, The fake user to be used in the configuration. + * @param {string} fakeauthorizedOrigins - authorizedOrigins to add to config. * @returns {object} - The configuration object. * @throws {TypeError} - Throws an error if the input is not a class. */ -function generateFakeConfig(serviceClass, fakeKey, fakeUser) { +function generateFakeConfig( + serviceClass, + fakeKey, + fakeUser, + fakeauthorizedOrigins, +) { if ( !serviceClass || !serviceClass.prototype || @@ -103,6 +109,9 @@ function generateFakeConfig(serviceClass, fakeKey, fakeUser) { if (!fakeKey || typeof fakeKey !== 'string') { throw new TypeError('Invalid fakeKey: Must be a String.') } + if (!fakeauthorizedOrigins || typeof fakeauthorizedOrigins !== 'string') { + throw new TypeError('Invalid fakeauthorizedOrigins: Must be a String.') + } if (!serviceClass.auth) { throw new Error(`Missing auth for ${serviceClass.name}.`) @@ -125,6 +134,13 @@ function generateFakeConfig(serviceClass, fakeKey, fakeUser) { // Build and return the configuration object with the fake key return { + public: { + services: { + [serviceClass.auth.serviceKey]: { + authorizedOrigins: [fakeauthorizedOrigins], + }, + }, + }, private: { [passKeyProperty]: fakeKey, [passUserProperty]: fakeUser, @@ -235,6 +251,36 @@ function getApiHeaderKeyOfService(serviceClass) { } } +/** + * Returns the first auth origin found for a provided service class. + * + * @param {BaseService} serviceClass The service class to find the authorized origins. + * @throws {TypeError} - Throws a TypeError if the input `serviceClass` is not an instance of BaseService. + * @returns {string} First auth origin found. + * + * @example + * // Example usage: + * getServiceClassAuthOrigin(Obs) + * // outputs 'https://api.opensuse.org' + */ +function getServiceClassAuthOrigin(serviceClass) { + if ( + !serviceClass || + !serviceClass.prototype || + !(serviceClass.prototype instanceof BaseService) + ) { + throw new TypeError( + `Invalid serviceClass ${serviceClass}: Must be an instance of BaseService.`, + ) + } + if (serviceClass.auth.authorizedOrigins) { + return serviceClass.auth.authorizedOrigins[0] + } else { + return config.public.services[serviceClass.auth.serviceKey] + .authorizedOrigins + } +} + /** * Generate a fake JWT Token valid for 1 hour for use in testing. * @@ -274,9 +320,14 @@ async function testAuth(serviceClass, dummyResponse) { const fakeUser = serviceClass.auth.userKey ? 'fake-user' : undefined const fakeSecret = 'fake-secret' const authMethod = getAuthMethod(serviceClass) - const config = generateFakeConfig(serviceClass, fakeSecret, fakeUser) + const authOrigin = getServiceClassAuthOrigin(serviceClass) + const config = generateFakeConfig( + serviceClass, + fakeSecret, + fakeUser, + authOrigin, + ) const exampleInvokeParams = getBadgeExampleCall(serviceClass) - const authOrigin = serviceClass.auth.authorizedOrigins[0] if (!authOrigin) { throw new TypeError(`Missing authorizedOrigins for ${serviceClass.name}.`) From b471c5c4f91cb27a113b6277359bebd08cda2667 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Mon, 12 Feb 2024 01:55:40 +0200 Subject: [PATCH 14/50] Add header optional argument --- services/test-helpers.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/services/test-helpers.js b/services/test-helpers.js index 25a44a07cb825..61c67c91b536c 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -302,13 +302,14 @@ function fakeJwtToken() { * @param {BaseService} serviceClass The service class tested. * @param {object} dummyResponse An object containing the dummy response by the server. * @throws {TypeError} - Throws a TypeError if the input `serviceClass` is not an instance of BaseService, + * @param {nock.ReplyHeaders} headers - Header for the response. * or if `serviceClass` is missing authorizedOrigins. * * @example * // Example usage: * testAuth(StackExchangeReputation, { items: [{ reputation: 8 }] }) */ -async function testAuth(serviceClass, dummyResponse) { +async function testAuth(serviceClass, dummyResponse, headers) { if (!(serviceClass.prototype instanceof BaseService)) { throw new TypeError( 'Invalid serviceClass: Must be an instance of BaseService.', @@ -339,14 +340,14 @@ async function testAuth(serviceClass, dummyResponse) { scope .get(/.*/) .basicAuth({ user: fakeUser, pass: fakeSecret }) - .reply(200, dummyResponse) + .reply(200, dummyResponse, headers) break case 'ApiKeyHeader': // TODO may fail if header is not default (see auth-helper.js - withApiKeyHeader) scope .get(/.*/) .matchHeader(getApiHeaderKeyOfService(serviceClass), fakeSecret) - .reply(200, dummyResponse) + .reply(200, dummyResponse, headers) break case 'BearerAuthHeader': scope @@ -355,23 +356,23 @@ async function testAuth(serviceClass, dummyResponse) { 'Authorization', `${getBearerPrefixOfService(serviceClass)} ${fakeSecret}`, ) - .reply(200, dummyResponse) + .reply(200, dummyResponse, headers) break case 'QueryStringAuth': scope .get(/.*/) .query(queryObject => queryObject.key === fakeSecret) - .reply(200, dummyResponse) + .reply(200, dummyResponse, headers) break case 'JwtAuth': { const fakeToken = fakeJwtToken() scope .post(/.*/, { username: fakeUser, password: fakeSecret }) - .reply(200, fakeToken) + .reply(200, fakeToken, headers) scope .get(/.*/) .matchHeader('Authorization', `Bearer ${fakeToken}`) - .reply(200, dummyResponse) + .reply(200, dummyResponse, headers) break } From 7aadc10b1d443fcba5999b90445b29437586aa02 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Mon, 12 Feb 2024 01:56:09 +0200 Subject: [PATCH 15/50] Add obs as basicAuth example --- services/obs/obs.spec.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 services/obs/obs.spec.js diff --git a/services/obs/obs.spec.js b/services/obs/obs.spec.js new file mode 100644 index 0000000000000..8e40b8fce4215 --- /dev/null +++ b/services/obs/obs.spec.js @@ -0,0 +1,15 @@ +import { testAuth } from '../test-helpers.js' +import ObsService from './obs.service.js' + +describe('ObsService', function () { + describe('auth', function () { + it('sends the auth information as configured', async function () { + return testAuth( + ObsService, + ` + `, + { 'Content-Type': 'application/xml' }, + ) + }) + }) +}) From 79dc536bd19f6535f6f2b8e332e6d1e7b5e66971 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Wed, 14 Feb 2024 00:41:48 +0200 Subject: [PATCH 16/50] Use apiHeaderKey and bearerHeaderKey function params Use apiHeaderKey & bearerHeaderKey as function params rather then extracting them with regex from function strings. Those options are now part of an options object param joined with the contentType that replaces header. header was originaly added for setting content type of the reply, so it makes more sense to directly set the content type --- services/discord/discord.spec.js | 7 +- services/obs/obs.spec.js | 3 +- .../stackexchange-monthlyquestions.spec.js | 4 +- .../stackexchange-reputation.spec.js | 4 +- .../stackexchange-taginfo.spec.js | 4 +- services/test-helpers.js | 153 ++++-------------- 6 files changed, 51 insertions(+), 124 deletions(-) diff --git a/services/discord/discord.spec.js b/services/discord/discord.spec.js index 328ea0b9e0477..4da1d14de5720 100644 --- a/services/discord/discord.spec.js +++ b/services/discord/discord.spec.js @@ -4,7 +4,12 @@ import Discord from './discord.service.js' describe('Discord', function () { describe('auth', function () { it('sends the auth information as configured', async function () { - return testAuth(Discord, { presence_count: 125 }) + return testAuth( + Discord, + 'BearerAuthHeader', + { presence_count: 125 }, + { bearerHeaderKey: 'Bot' }, + ) }) }) }) diff --git a/services/obs/obs.spec.js b/services/obs/obs.spec.js index 8e40b8fce4215..fa19d9868b2f9 100644 --- a/services/obs/obs.spec.js +++ b/services/obs/obs.spec.js @@ -6,9 +6,10 @@ describe('ObsService', function () { it('sends the auth information as configured', async function () { return testAuth( ObsService, + 'BasicAuth', ` `, - { 'Content-Type': 'application/xml' }, + { contentType: 'application/xml' }, ) }) }) diff --git a/services/stackexchange/stackexchange-monthlyquestions.spec.js b/services/stackexchange/stackexchange-monthlyquestions.spec.js index 45f4dadf8b208..ae6cf7581dae5 100644 --- a/services/stackexchange/stackexchange-monthlyquestions.spec.js +++ b/services/stackexchange/stackexchange-monthlyquestions.spec.js @@ -4,7 +4,9 @@ import StackExchangeMonthlyQuestions from './stackexchange-monthlyquestions.serv describe('StackExchangeMonthlyQuestions', function () { describe('auth', function () { it('sends the auth information as configured', async function () { - return testAuth(StackExchangeMonthlyQuestions, { total: 8 }) + return testAuth(StackExchangeMonthlyQuestions, 'QueryStringAuth', { + total: 8, + }) }) }) }) diff --git a/services/stackexchange/stackexchange-reputation.spec.js b/services/stackexchange/stackexchange-reputation.spec.js index 5b1625e6d7d23..d69a50c84e535 100644 --- a/services/stackexchange/stackexchange-reputation.spec.js +++ b/services/stackexchange/stackexchange-reputation.spec.js @@ -4,7 +4,9 @@ import StackExchangeReputation from './stackexchange-reputation.service.js' describe('StackExchangeReputation', function () { describe('auth', function () { it('sends the auth information as configured', async function () { - return testAuth(StackExchangeReputation, { items: [{ reputation: 8 }] }) + return testAuth(StackExchangeReputation, 'QueryStringAuth', { + items: [{ reputation: 8 }], + }) }) }) }) diff --git a/services/stackexchange/stackexchange-taginfo.spec.js b/services/stackexchange/stackexchange-taginfo.spec.js index b53b8c968ab76..ee5a20edbc5bf 100644 --- a/services/stackexchange/stackexchange-taginfo.spec.js +++ b/services/stackexchange/stackexchange-taginfo.spec.js @@ -4,7 +4,9 @@ import StackExchangeQuestions from './stackexchange-taginfo.service.js' describe('StackExchangeQuestions', function () { describe('auth', function () { it('sends the auth information as configured', async function () { - return testAuth(StackExchangeQuestions, { items: [{ count: 8 }] }) + return testAuth(StackExchangeQuestions, 'QueryStringAuth', { + items: [{ count: 8 }], + }) }) }) }) diff --git a/services/test-helpers.js b/services/test-helpers.js index 61c67c91b536c..3e3facfeaa5ac 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -148,109 +148,6 @@ function generateFakeConfig( } } -/** - * Returns the name of the auth method of the service class provided - * - * @param {BaseService} serviceClass The service class to extract auth method from. - * @throws {TypeError} - Throws a TypeError if the input `serviceClass` is not an instance of BaseService - * or if the authHelper function is missing in fetch. - * @returns {string} The auth method of the service class provided. - * May return on of the following strings: BasicAuth, ApiKeyHeader, BearerAuthHeader, QueryStringAuth, JwtAuth. - * - * @example - * // Example usage: - * getAuthMethod(SonarCoverage) - * // outputs "BasicAuth" - */ -function getAuthMethod(serviceClass) { - if ( - !serviceClass || - !serviceClass.prototype || - !(serviceClass.prototype instanceof BaseService) - ) { - throw new TypeError( - `Invalid serviceClass ${serviceClass}: Must be an instance of BaseService.`, - ) - } - const fetchFunctionString = serviceClass.prototype.fetch.toString() - const result = fetchFunctionString.match( - /this\.authHelper\.with([A-Z][a-zA-Z0-9_]+)\(/, - ) - if (result) { - return result[1] - } else { - throw new TypeError( - 'Invalid serviceClass: Missing authHelper function in fetch.', - ) - } -} - -/** - * Returns the prefix of the bearer token for a service class. - * - * @param {BaseService} serviceClass The service class to extract auth method from. - * @throws {TypeError} - Throws a TypeError if the input `serviceClass` is not an instance of BaseService. - * @returns {string} Bearer token prefix. - * - * @example - * // Example usage: - * getBearerPrefixOfService(Discord) - * // outputs 'Bot' - */ -function getBearerPrefixOfService(serviceClass) { - if ( - !serviceClass || - !serviceClass.prototype || - !(serviceClass.prototype instanceof BaseService) - ) { - throw new TypeError( - `Invalid serviceClass ${serviceClass}: Must be an instance of BaseService.`, - ) - } - const fetchFunctionString = serviceClass.prototype.fetch.toString() - const result = fetchFunctionString.match( - /withBearerAuthHeader\([\s\S]*,\s+['"`]([\s\S]*)['"`],?\s*\)/, - ) - if (result) { - return result[1] - } else { - return 'Bearer' - } -} - -/** - * Returns the prefix of the bearer token for a service class. - * - * @param {BaseService} serviceClass The service class to extract auth method from. - * @throws {TypeError} - Throws a TypeError if the input `serviceClass` is not an instance of BaseService. - * @returns {string} Bearer token prefix. - * - * @example - * // Example usage: - * getApiHeaderKeyOfService(CurseForgeDownloads) - * // outputs 'x-api-key' - */ -function getApiHeaderKeyOfService(serviceClass) { - if ( - !serviceClass || - !serviceClass.prototype || - !(serviceClass.prototype instanceof BaseService) - ) { - throw new TypeError( - `Invalid serviceClass ${serviceClass}: Must be an instance of BaseService.`, - ) - } - const fetchFunctionString = serviceClass.prototype.fetch.toString() - const result = fetchFunctionString.match( - /withApiKeyHeader\([\s\S]*,\s+['"`]([\s\S]*)['"`],?\s*\)/, - ) - if (result) { - return result[1] - } else { - return 'x-api-key' - } -} - /** * Returns the first auth origin found for a provided service class. * @@ -297,19 +194,23 @@ function fakeJwtToken() { } /** - * Test authentication of a badge for it's first OpenAPI example using a provided dummyResponse + * Test authentication of a badge for it's first OpenAPI example using a provided dummyResponse and authentication method. * * @param {BaseService} serviceClass The service class tested. + * @param {'BasicAuth'|'ApiKeyHeader'|'BearerAuthHeader'|'QueryStringAuth'|'JwtAuth'} authMethod The auth method of the tested service class. * @param {object} dummyResponse An object containing the dummy response by the server. + * @param {object} options - Additional options for non default keys and content-type of the dummy response. + * @param {'application/xml'|'application/json'} options.contentType - Header for the response, may contain any string. + * @param {string} options.apiHeaderKey - Non default header for ApiKeyHeader auth. + * @param {string} options.bearerHeaderKey - Non default bearer header prefix for BearerAuthHeader. * @throws {TypeError} - Throws a TypeError if the input `serviceClass` is not an instance of BaseService, - * @param {nock.ReplyHeaders} headers - Header for the response. * or if `serviceClass` is missing authorizedOrigins. * * @example * // Example usage: - * testAuth(StackExchangeReputation, { items: [{ reputation: 8 }] }) + * testAuth(StackExchangeReputation, QueryStringAuth, { items: [{ reputation: 8 }] }) */ -async function testAuth(serviceClass, dummyResponse, headers) { +async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { if (!(serviceClass.prototype instanceof BaseService)) { throw new TypeError( 'Invalid serviceClass: Must be an instance of BaseService.', @@ -320,7 +221,6 @@ async function testAuth(serviceClass, dummyResponse, headers) { const fakeUser = serviceClass.auth.userKey ? 'fake-user' : undefined const fakeSecret = 'fake-secret' - const authMethod = getAuthMethod(serviceClass) const authOrigin = getServiceClassAuthOrigin(serviceClass) const config = generateFakeConfig( serviceClass, @@ -329,6 +229,24 @@ async function testAuth(serviceClass, dummyResponse, headers) { authOrigin, ) const exampleInvokeParams = getBadgeExampleCall(serviceClass) + if (options && typeof options !== 'object') { + throw new TypeError('Invalid options: Must be an object.') + } + const { + contentType, + apiHeaderKey = 'x-api-key', + bearerHeaderKey = 'Bearer', + } = options + if (contentType && typeof contentType !== 'string') { + throw new TypeError('Invalid contentType: Must be a String.') + } + const header = contentType ? { 'Content-Type': contentType } : undefined + if (!apiHeaderKey || typeof apiHeaderKey !== 'string') { + throw new TypeError('Invalid apiHeaderKey: Must be a String.') + } + if (!bearerHeaderKey || typeof bearerHeaderKey !== 'string') { + throw new TypeError('Invalid bearerHeaderKey: Must be a String.') + } if (!authOrigin) { throw new TypeError(`Missing authorizedOrigins for ${serviceClass.name}.`) @@ -340,39 +258,36 @@ async function testAuth(serviceClass, dummyResponse, headers) { scope .get(/.*/) .basicAuth({ user: fakeUser, pass: fakeSecret }) - .reply(200, dummyResponse, headers) + .reply(200, dummyResponse, header) break case 'ApiKeyHeader': // TODO may fail if header is not default (see auth-helper.js - withApiKeyHeader) scope .get(/.*/) - .matchHeader(getApiHeaderKeyOfService(serviceClass), fakeSecret) - .reply(200, dummyResponse, headers) + .matchHeader(apiHeaderKey, fakeSecret) + .reply(200, dummyResponse, header) break case 'BearerAuthHeader': scope .get(/.*/) - .matchHeader( - 'Authorization', - `${getBearerPrefixOfService(serviceClass)} ${fakeSecret}`, - ) - .reply(200, dummyResponse, headers) + .matchHeader('Authorization', `${bearerHeaderKey} ${fakeSecret}`) + .reply(200, dummyResponse, header) break case 'QueryStringAuth': scope .get(/.*/) .query(queryObject => queryObject.key === fakeSecret) - .reply(200, dummyResponse, headers) + .reply(200, dummyResponse, header) break case 'JwtAuth': { const fakeToken = fakeJwtToken() scope .post(/.*/, { username: fakeUser, password: fakeSecret }) - .reply(200, fakeToken, headers) + .reply(200, fakeToken, header) scope .get(/.*/) .matchHeader('Authorization', `Bearer ${fakeToken}`) - .reply(200, dummyResponse, headers) + .reply(200, dummyResponse, header) break } From d1435c26f54738530c0daca22ec234e294dafbd2 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Wed, 14 Feb 2024 00:49:28 +0200 Subject: [PATCH 17/50] Remove old comment --- services/test-helpers.js | 1 - 1 file changed, 1 deletion(-) diff --git a/services/test-helpers.js b/services/test-helpers.js index 3e3facfeaa5ac..23cee799c5279 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -261,7 +261,6 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { .reply(200, dummyResponse, header) break case 'ApiKeyHeader': - // TODO may fail if header is not default (see auth-helper.js - withApiKeyHeader) scope .get(/.*/) .matchHeader(apiHeaderKey, fakeSecret) From a53f71654a2022e277d24933654b95432c1e5d3a Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Fri, 16 Feb 2024 13:08:16 +0200 Subject: [PATCH 18/50] Allow any pass & user key for QueryStringAuth Before this commit the QueryStringAuth would only work for the key of stackexchange. This commit makes the testAuth function generic and allows passing user and pass keys. --- .../stackexchange-monthlyquestions.spec.js | 11 +++++++--- .../stackexchange-reputation.spec.js | 11 +++++++--- .../stackexchange-taginfo.spec.js | 11 +++++++--- services/test-helpers.js | 22 ++++++++++++++++++- 4 files changed, 45 insertions(+), 10 deletions(-) diff --git a/services/stackexchange/stackexchange-monthlyquestions.spec.js b/services/stackexchange/stackexchange-monthlyquestions.spec.js index ae6cf7581dae5..b014718eaefc0 100644 --- a/services/stackexchange/stackexchange-monthlyquestions.spec.js +++ b/services/stackexchange/stackexchange-monthlyquestions.spec.js @@ -4,9 +4,14 @@ import StackExchangeMonthlyQuestions from './stackexchange-monthlyquestions.serv describe('StackExchangeMonthlyQuestions', function () { describe('auth', function () { it('sends the auth information as configured', async function () { - return testAuth(StackExchangeMonthlyQuestions, 'QueryStringAuth', { - total: 8, - }) + return testAuth( + StackExchangeMonthlyQuestions, + 'QueryStringAuth', + { + total: 8, + }, + { queryPassKey: 'key' }, + ) }) }) }) diff --git a/services/stackexchange/stackexchange-reputation.spec.js b/services/stackexchange/stackexchange-reputation.spec.js index d69a50c84e535..b0bcd9be17732 100644 --- a/services/stackexchange/stackexchange-reputation.spec.js +++ b/services/stackexchange/stackexchange-reputation.spec.js @@ -4,9 +4,14 @@ import StackExchangeReputation from './stackexchange-reputation.service.js' describe('StackExchangeReputation', function () { describe('auth', function () { it('sends the auth information as configured', async function () { - return testAuth(StackExchangeReputation, 'QueryStringAuth', { - items: [{ reputation: 8 }], - }) + return testAuth( + StackExchangeReputation, + 'QueryStringAuth', + { + items: [{ reputation: 8 }], + }, + { queryPassKey: 'key' }, + ) }) }) }) diff --git a/services/stackexchange/stackexchange-taginfo.spec.js b/services/stackexchange/stackexchange-taginfo.spec.js index ee5a20edbc5bf..46977c1ced29b 100644 --- a/services/stackexchange/stackexchange-taginfo.spec.js +++ b/services/stackexchange/stackexchange-taginfo.spec.js @@ -4,9 +4,14 @@ import StackExchangeQuestions from './stackexchange-taginfo.service.js' describe('StackExchangeQuestions', function () { describe('auth', function () { it('sends the auth information as configured', async function () { - return testAuth(StackExchangeQuestions, 'QueryStringAuth', { - items: [{ count: 8 }], - }) + return testAuth( + StackExchangeQuestions, + 'QueryStringAuth', + { + items: [{ count: 8 }], + }, + { queryPassKey: 'key' }, + ) }) }) }) diff --git a/services/test-helpers.js b/services/test-helpers.js index 23cee799c5279..09064903f1043 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -203,6 +203,8 @@ function fakeJwtToken() { * @param {'application/xml'|'application/json'} options.contentType - Header for the response, may contain any string. * @param {string} options.apiHeaderKey - Non default header for ApiKeyHeader auth. * @param {string} options.bearerHeaderKey - Non default bearer header prefix for BearerAuthHeader. + * @param {string} options.queryUserKey - QueryStringAuth user key. + * @param {string} options.queryPassKey - QueryStringAuth pass key. * @throws {TypeError} - Throws a TypeError if the input `serviceClass` is not an instance of BaseService, * or if `serviceClass` is missing authorizedOrigins. * @@ -236,6 +238,8 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { contentType, apiHeaderKey = 'x-api-key', bearerHeaderKey = 'Bearer', + queryUserKey, + queryPassKey, } = options if (contentType && typeof contentType !== 'string') { throw new TypeError('Invalid contentType: Must be a String.') @@ -273,9 +277,25 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { .reply(200, dummyResponse, header) break case 'QueryStringAuth': + if (!queryPassKey || typeof queryPassKey !== 'string') { + throw new TypeError('Invalid queryPassKey: Must be a String.') + } scope .get(/.*/) - .query(queryObject => queryObject.key === fakeSecret) + .query(queryObject => { + if (queryObject[queryPassKey] !== fakeSecret) { + return false + } + if (queryUserKey) { + if (typeof queryUserKey !== 'string') { + throw new TypeError('Invalid queryUserKey: Must be a String.') + } + if (queryObject[queryUserKey] !== fakeUser) { + return false + } + } + return true + }) .reply(200, dummyResponse, header) break case 'JwtAuth': { From 14d07899e0376bd1ad5434f8f21f1d9020266d87 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Fri, 16 Feb 2024 22:01:34 +0200 Subject: [PATCH 19/50] Add auth test for PepyDownloads --- services/pepy/pepy-downloads.spec.js | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 services/pepy/pepy-downloads.spec.js diff --git a/services/pepy/pepy-downloads.spec.js b/services/pepy/pepy-downloads.spec.js new file mode 100644 index 0000000000000..9a529af63cf1f --- /dev/null +++ b/services/pepy/pepy-downloads.spec.js @@ -0,0 +1,10 @@ +import { testAuth } from '../test-helpers.js' +import PepyDownloads from './pepy-downloads.service.js' + +describe('PepyDownloads', function () { + describe('auth', function () { + it('sends the auth information as configured', async function () { + return testAuth(PepyDownloads, 'ApiKeyHeader', { total_downloads: 42 }) + }) + }) +}) From d22de8a1cdf4bf587f3cb08ff00098ac11255f34 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Fri, 16 Feb 2024 23:57:26 +0200 Subject: [PATCH 20/50] Fix wrong header for jwt login Might set wrong header for jwt login request. This commit fixes that. --- services/test-helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/test-helpers.js b/services/test-helpers.js index 09064903f1043..e1680d9e32a41 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -302,7 +302,7 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { const fakeToken = fakeJwtToken() scope .post(/.*/, { username: fakeUser, password: fakeSecret }) - .reply(200, fakeToken, header) + .reply(200, { token: fakeToken }) scope .get(/.*/) .matchHeader('Authorization', `Bearer ${fakeToken}`) From 50f41441908b3856abea0eedd31030fff52434ce Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sat, 17 Feb 2024 00:23:28 +0200 Subject: [PATCH 21/50] Support multiple authOrigins in testAuth Some services might have more then one authOrigin. This commit makes sure we test for redundent authOrigins as well as support requests to them if needed. --- services/test-helpers.js | 134 ++++++++++++++++++++------------------- 1 file changed, 70 insertions(+), 64 deletions(-) diff --git a/services/test-helpers.js b/services/test-helpers.js index e1680d9e32a41..cfa488b0f31e2 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -109,8 +109,8 @@ function generateFakeConfig( if (!fakeKey || typeof fakeKey !== 'string') { throw new TypeError('Invalid fakeKey: Must be a String.') } - if (!fakeauthorizedOrigins || typeof fakeauthorizedOrigins !== 'string') { - throw new TypeError('Invalid fakeauthorizedOrigins: Must be a String.') + if (!fakeauthorizedOrigins || typeof fakeauthorizedOrigins !== 'object') { + throw new TypeError('Invalid fakeauthorizedOrigins: Must be an array.') } if (!serviceClass.auth) { @@ -137,7 +137,7 @@ function generateFakeConfig( public: { services: { [serviceClass.auth.serviceKey]: { - authorizedOrigins: [fakeauthorizedOrigins], + authorizedOrigins: fakeauthorizedOrigins, }, }, }, @@ -171,10 +171,11 @@ function getServiceClassAuthOrigin(serviceClass) { ) } if (serviceClass.auth.authorizedOrigins) { - return serviceClass.auth.authorizedOrigins[0] + return serviceClass.auth.authorizedOrigins } else { - return config.public.services[serviceClass.auth.serviceKey] - .authorizedOrigins + return [ + config.public.services[serviceClass.auth.serviceKey].authorizedOrigins, + ] } } @@ -223,12 +224,12 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { const fakeUser = serviceClass.auth.userKey ? 'fake-user' : undefined const fakeSecret = 'fake-secret' - const authOrigin = getServiceClassAuthOrigin(serviceClass) + const authOrigins = getServiceClassAuthOrigin(serviceClass) const config = generateFakeConfig( serviceClass, fakeSecret, fakeUser, - authOrigin, + authOrigins, ) const exampleInvokeParams = getBadgeExampleCall(serviceClass) if (options && typeof options !== 'object') { @@ -252,73 +253,78 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { throw new TypeError('Invalid bearerHeaderKey: Must be a String.') } - if (!authOrigin) { + if (!authOrigins) { throw new TypeError(`Missing authorizedOrigins for ${serviceClass.name}.`) } - const scope = nock(authOrigin) - switch (authMethod) { - case 'BasicAuth': - scope - .get(/.*/) - .basicAuth({ user: fakeUser, pass: fakeSecret }) - .reply(200, dummyResponse, header) - break - case 'ApiKeyHeader': - scope - .get(/.*/) - .matchHeader(apiHeaderKey, fakeSecret) - .reply(200, dummyResponse, header) - break - case 'BearerAuthHeader': - scope - .get(/.*/) - .matchHeader('Authorization', `${bearerHeaderKey} ${fakeSecret}`) - .reply(200, dummyResponse, header) - break - case 'QueryStringAuth': - if (!queryPassKey || typeof queryPassKey !== 'string') { - throw new TypeError('Invalid queryPassKey: Must be a String.') - } - scope - .get(/.*/) - .query(queryObject => { - if (queryObject[queryPassKey] !== fakeSecret) { - return false - } - if (queryUserKey) { - if (typeof queryUserKey !== 'string') { - throw new TypeError('Invalid queryUserKey: Must be a String.') - } - if (queryObject[queryUserKey] !== fakeUser) { + const scopeArr = [] + authOrigins.forEach(authOrigin => { + const scope = nock(authOrigin) + scopeArr.push(scope) + switch (authMethod) { + case 'BasicAuth': + scope + .get(/.*/) + .basicAuth({ user: fakeUser, pass: fakeSecret }) + .reply(200, dummyResponse, header) + break + case 'ApiKeyHeader': + scope + .get(/.*/) + .matchHeader(apiHeaderKey, fakeSecret) + .reply(200, dummyResponse, header) + break + case 'BearerAuthHeader': + scope + .get(/.*/) + .matchHeader('Authorization', `${bearerHeaderKey} ${fakeSecret}`) + .reply(200, dummyResponse, header) + break + case 'QueryStringAuth': + if (!queryPassKey || typeof queryPassKey !== 'string') { + throw new TypeError('Invalid queryPassKey: Must be a String.') + } + scope + .get(/.*/) + .query(queryObject => { + if (queryObject[queryPassKey] !== fakeSecret) { return false } - } - return true - }) - .reply(200, dummyResponse, header) - break - case 'JwtAuth': { - const fakeToken = fakeJwtToken() - scope - .post(/.*/, { username: fakeUser, password: fakeSecret }) - .reply(200, { token: fakeToken }) - scope - .get(/.*/) - .matchHeader('Authorization', `Bearer ${fakeToken}`) - .reply(200, dummyResponse, header) - break - } + if (queryUserKey) { + if (typeof queryUserKey !== 'string') { + throw new TypeError('Invalid queryUserKey: Must be a String.') + } + if (queryObject[queryUserKey] !== fakeUser) { + return false + } + } + return true + }) + .reply(200, dummyResponse, header) + break + case 'JwtAuth': { + const fakeToken = fakeJwtToken() + scope + .post(/.*/, { username: fakeUser, password: fakeSecret }) + .reply(200, { token: fakeToken }) + scope + .get(/.*/) + .matchHeader('Authorization', `Bearer ${fakeToken}`) + .reply(200, dummyResponse, header) + break + } - default: - throw new TypeError(`Unkown auth method for ${serviceClass.name}.`) - } + default: + throw new TypeError(`Unkown auth method for ${serviceClass.name}.`) + } + }) expect( await serviceClass.invoke(defaultContext, config, exampleInvokeParams), ).to.not.have.property('isError') - scope.done() + // if we get 'Mocks not yet satisfied' we have redundent authOrigins or we are missing a critical request + scopeArr.forEach(scope => scope.done()) } const defaultContext = { requestFetcher: fetch } From 2d310bda8fe6b3eccb296a214fe71b0f1198cf95 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sat, 17 Feb 2024 00:23:53 +0200 Subject: [PATCH 22/50] Add docker-automated auth test --- services/docker/docker-automated.spec.js | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 services/docker/docker-automated.spec.js diff --git a/services/docker/docker-automated.spec.js b/services/docker/docker-automated.spec.js new file mode 100644 index 0000000000000..cafc7d6b71212 --- /dev/null +++ b/services/docker/docker-automated.spec.js @@ -0,0 +1,10 @@ +import { testAuth } from '../test-helpers.js' +import DockerAutomatedBuild from './docker-automated.service.js' + +describe('DockerAutomatedBuild', function () { + describe('auth', function () { + it('sends the auth information as configured', async function () { + return testAuth(DockerAutomatedBuild, 'JwtAuth', { is_automated: true }) + }) + }) +}) From 1b79b4cf4478c5438f42b5308d250229435cb94d Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sat, 17 Feb 2024 10:51:23 +0200 Subject: [PATCH 23/50] Fix JwtAuth testing by introducing mandatory jwtLoginEndpoint Prior to this change, JwtAuth testing would lead to erros due to the absence of a specified login endpoint, Nock would be dumplicated for both login and non login hosts and indicate a missing request. This commit enforces the requirement for a new jwtLoginEndpoint argument when testing JwtAuth. The argument seperates the endpoint nock scope from the behavior of the request nock. --- services/docker/docker-automated.spec.js | 7 ++++++- services/test-helpers.js | 24 ++++++++++++++++-------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/services/docker/docker-automated.spec.js b/services/docker/docker-automated.spec.js index cafc7d6b71212..5d9ce4801b8ac 100644 --- a/services/docker/docker-automated.spec.js +++ b/services/docker/docker-automated.spec.js @@ -4,7 +4,12 @@ import DockerAutomatedBuild from './docker-automated.service.js' describe('DockerAutomatedBuild', function () { describe('auth', function () { it('sends the auth information as configured', async function () { - return testAuth(DockerAutomatedBuild, 'JwtAuth', { is_automated: true }) + return testAuth( + DockerAutomatedBuild, + 'JwtAuth', + { is_automated: true }, + { jwtLoginEndpoint: 'https://hub.docker.com/v2/users/login/' }, + ) }) }) }) diff --git a/services/test-helpers.js b/services/test-helpers.js index cfa488b0f31e2..e4257c1231b29 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -206,6 +206,7 @@ function fakeJwtToken() { * @param {string} options.bearerHeaderKey - Non default bearer header prefix for BearerAuthHeader. * @param {string} options.queryUserKey - QueryStringAuth user key. * @param {string} options.queryPassKey - QueryStringAuth pass key. + * @param {string} options.jwtLoginEndpoint - jwtAuth Login endpoint. * @throws {TypeError} - Throws a TypeError if the input `serviceClass` is not an instance of BaseService, * or if `serviceClass` is missing authorizedOrigins. * @@ -241,6 +242,7 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { bearerHeaderKey = 'Bearer', queryUserKey, queryPassKey, + jwtLoginEndpoint, } = options if (contentType && typeof contentType !== 'string') { throw new TypeError('Invalid contentType: Must be a String.') @@ -256,6 +258,7 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { if (!authOrigins) { throw new TypeError(`Missing authorizedOrigins for ${serviceClass.name}.`) } + const jwtToken = authMethod === 'JwtAuth' ? fakeJwtToken() : undefined const scopeArr = [] authOrigins.forEach(authOrigin => { @@ -303,14 +306,19 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { .reply(200, dummyResponse, header) break case 'JwtAuth': { - const fakeToken = fakeJwtToken() - scope - .post(/.*/, { username: fakeUser, password: fakeSecret }) - .reply(200, { token: fakeToken }) - scope - .get(/.*/) - .matchHeader('Authorization', `Bearer ${fakeToken}`) - .reply(200, dummyResponse, header) + if (!jwtLoginEndpoint || typeof jwtLoginEndpoint !== 'string') { + throw new TypeError('Invalid jwtLoginEndpoint: Must be a String.') + } + if (jwtLoginEndpoint.startsWith(authOrigin)) { + scope + .post(/.*/, { username: fakeUser, password: fakeSecret }) + .reply(200, { token: jwtToken }) + } else { + scope + .get(/.*/) + .matchHeader('Authorization', `Bearer ${jwtToken}`) + .reply(200, dummyResponse, header) + } break } From a2b838c4575d3a8edef19f8c01f0e3555c063d42 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Tue, 20 Feb 2024 23:59:12 +0200 Subject: [PATCH 24/50] Fix type test in generateFakeConfig --- services/test-helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/test-helpers.js b/services/test-helpers.js index e4257c1231b29..b1368389c33c5 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -109,7 +109,7 @@ function generateFakeConfig( if (!fakeKey || typeof fakeKey !== 'string') { throw new TypeError('Invalid fakeKey: Must be a String.') } - if (!fakeauthorizedOrigins || typeof fakeauthorizedOrigins !== 'object') { + if (!fakeauthorizedOrigins || !Array.isArray(fakeauthorizedOrigins)) { throw new TypeError('Invalid fakeauthorizedOrigins: Must be an array.') } From 4d57607763f6eadba4f7d52ed167d757c3ae74b2 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Thu, 22 Feb 2024 23:07:24 +0200 Subject: [PATCH 25/50] Add exampleOverride to testAuth Introduce `exampleOverride` to test authentication scenarios. Allows providing custom examples for testAuth via an object. Simulates service requests that trigger desiered condition paths in tests. --- services/test-helpers.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/services/test-helpers.js b/services/test-helpers.js index b1368389c33c5..71a76549afd97 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -207,6 +207,7 @@ function fakeJwtToken() { * @param {string} options.queryUserKey - QueryStringAuth user key. * @param {string} options.queryPassKey - QueryStringAuth pass key. * @param {string} options.jwtLoginEndpoint - jwtAuth Login endpoint. + * @param {object} options.exampleOverride - Override example params in test. * @throws {TypeError} - Throws a TypeError if the input `serviceClass` is not an instance of BaseService, * or if `serviceClass` is missing authorizedOrigins. * @@ -243,6 +244,7 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { queryUserKey, queryPassKey, jwtLoginEndpoint, + exampleOverride = {}, } = options if (contentType && typeof contentType !== 'string') { throw new TypeError('Invalid contentType: Must be a String.') @@ -254,6 +256,9 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { if (!bearerHeaderKey || typeof bearerHeaderKey !== 'string') { throw new TypeError('Invalid bearerHeaderKey: Must be a String.') } + if (!exampleOverride || typeof exampleOverride !== 'object') { + throw new TypeError('Invalid exampleOverride: Must be an Object.') + } if (!authOrigins) { throw new TypeError(`Missing authorizedOrigins for ${serviceClass.name}.`) @@ -328,7 +333,10 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { }) expect( - await serviceClass.invoke(defaultContext, config, exampleInvokeParams), + await serviceClass.invoke(defaultContext, config, { + ...exampleInvokeParams, + ...exampleOverride, + }), ).to.not.have.property('isError') // if we get 'Mocks not yet satisfied' we have redundent authOrigins or we are missing a critical request From f156762e26a95e0687492b722872f15f97703b9c Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Fri, 23 Feb 2024 22:33:20 +0200 Subject: [PATCH 26/50] Add authOverride to testAuth Some classes can change the auth based on badge inputs. This parameter allows the flexability of overriding the default auth for testing those cases. --- services/test-helpers.js | 63 +++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/services/test-helpers.js b/services/test-helpers.js index 71a76549afd97..f44df19fc0eaa 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -88,6 +88,7 @@ function getBadgeExampleCall(serviceClass) { * @param {string} fakeKey - The fake key to be used in the configuration. * @param {string} fakeUser - Optional, The fake user to be used in the configuration. * @param {string} fakeauthorizedOrigins - authorizedOrigins to add to config. + * @param {object} authOverride Return result with overrid params. * @returns {object} - The configuration object. * @throws {TypeError} - Throws an error if the input is not a class. */ @@ -96,6 +97,7 @@ function generateFakeConfig( fakeKey, fakeUser, fakeauthorizedOrigins, + authOverride, ) { if ( !serviceClass || @@ -113,30 +115,31 @@ function generateFakeConfig( throw new TypeError('Invalid fakeauthorizedOrigins: Must be an array.') } - if (!serviceClass.auth) { - throw new Error(`Missing auth for ${serviceClass.name}.`) + const auth = { ...serviceClass.auth, ...authOverride } + if (Object.keys(auth).length === 0) { + throw new Error(`Auth empty for ${serviceClass.name}.`) } - if (!serviceClass.auth.passKey) { + if (!auth.passKey) { throw new Error(`Missing auth.passKey for ${serviceClass.name}.`) } // Extract the passKey property from auth, or use a default if not present - const passKeyProperty = serviceClass.auth.passKey + const passKeyProperty = auth.passKey let passUserProperty = 'placeholder' if (fakeUser) { if (typeof fakeKey !== 'string') { throw new TypeError('Invalid fakeUser: Must be a String.') } - if (!serviceClass.auth.userKey) { + if (!auth.userKey) { throw new Error(`Missing auth.userKey for ${serviceClass.name}.`) } - passUserProperty = serviceClass.auth.userKey + passUserProperty = auth.userKey } // Build and return the configuration object with the fake key return { public: { services: { - [serviceClass.auth.serviceKey]: { + [auth.serviceKey]: { authorizedOrigins: fakeauthorizedOrigins, }, }, @@ -152,6 +155,7 @@ function generateFakeConfig( * Returns the first auth origin found for a provided service class. * * @param {BaseService} serviceClass The service class to find the authorized origins. + * @param {object} authOverride Return result with overrid params. * @throws {TypeError} - Throws a TypeError if the input `serviceClass` is not an instance of BaseService. * @returns {string} First auth origin found. * @@ -160,7 +164,7 @@ function generateFakeConfig( * getServiceClassAuthOrigin(Obs) * // outputs 'https://api.opensuse.org' */ -function getServiceClassAuthOrigin(serviceClass) { +function getServiceClassAuthOrigin(serviceClass, authOverride) { if ( !serviceClass || !serviceClass.prototype || @@ -170,12 +174,11 @@ function getServiceClassAuthOrigin(serviceClass) { `Invalid serviceClass ${serviceClass}: Must be an instance of BaseService.`, ) } - if (serviceClass.auth.authorizedOrigins) { + const auth = { ...serviceClass.auth, ...authOverride } + if (auth.authorizedOrigins) { return serviceClass.auth.authorizedOrigins } else { - return [ - config.public.services[serviceClass.auth.serviceKey].authorizedOrigins, - ] + return [config.public.services[auth.serviceKey].authorizedOrigins] } } @@ -208,6 +211,7 @@ function fakeJwtToken() { * @param {string} options.queryPassKey - QueryStringAuth pass key. * @param {string} options.jwtLoginEndpoint - jwtAuth Login endpoint. * @param {object} options.exampleOverride - Override example params in test. + * @param {object} options.authOverride - Override class auth params. * @throws {TypeError} - Throws a TypeError if the input `serviceClass` is not an instance of BaseService, * or if `serviceClass` is missing authorizedOrigins. * @@ -224,19 +228,6 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { cleanUpNockAfterEach() - const fakeUser = serviceClass.auth.userKey ? 'fake-user' : undefined - const fakeSecret = 'fake-secret' - const authOrigins = getServiceClassAuthOrigin(serviceClass) - const config = generateFakeConfig( - serviceClass, - fakeSecret, - fakeUser, - authOrigins, - ) - const exampleInvokeParams = getBadgeExampleCall(serviceClass) - if (options && typeof options !== 'object') { - throw new TypeError('Invalid options: Must be an object.') - } const { contentType, apiHeaderKey = 'x-api-key', @@ -245,6 +236,7 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { queryPassKey, jwtLoginEndpoint, exampleOverride = {}, + authOverride, } = options if (contentType && typeof contentType !== 'string') { throw new TypeError('Invalid contentType: Must be a String.') @@ -256,9 +248,28 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { if (!bearerHeaderKey || typeof bearerHeaderKey !== 'string') { throw new TypeError('Invalid bearerHeaderKey: Must be a String.') } - if (!exampleOverride || typeof exampleOverride !== 'object') { + if (typeof exampleOverride !== 'object') { throw new TypeError('Invalid exampleOverride: Must be an Object.') } + if (authOverride && typeof authOverride !== 'object') { + throw new TypeError('Invalid authOverride: Must be an Object.') + } + + const auth = { ...serviceClass.auth, ...authOverride } + const fakeUser = auth.userKey ? 'fake-user' : undefined + const fakeSecret = 'fake-secret' + const authOrigins = getServiceClassAuthOrigin(serviceClass, authOverride) + const config = generateFakeConfig( + serviceClass, + fakeSecret, + fakeUser, + authOrigins, + authOverride, + ) + const exampleInvokeParams = getBadgeExampleCall(serviceClass) + if (options && typeof options !== 'object') { + throw new TypeError('Invalid options: Must be an object.') + } if (!authOrigins) { throw new TypeError(`Missing authorizedOrigins for ${serviceClass.name}.`) From cd6c65b9afbc04f1acabe8a8b62ea3c19548175d Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Fri, 23 Feb 2024 23:30:30 +0200 Subject: [PATCH 27/50] Add configOverride to testAuth Some tests might require non default config for cases where these are set. This allows creating tests for those cases. --- services/test-helpers.js | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/services/test-helpers.js b/services/test-helpers.js index f44df19fc0eaa..3d18480e9cf58 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -1,3 +1,4 @@ +import _ from 'lodash' import dayjs from 'dayjs' import { expect } from 'chai' import nock from 'nock' @@ -156,6 +157,7 @@ function generateFakeConfig( * * @param {BaseService} serviceClass The service class to find the authorized origins. * @param {object} authOverride Return result with overrid params. + * @param {object} configOverride - Override the config. * @throws {TypeError} - Throws a TypeError if the input `serviceClass` is not an instance of BaseService. * @returns {string} First auth origin found. * @@ -164,7 +166,7 @@ function generateFakeConfig( * getServiceClassAuthOrigin(Obs) * // outputs 'https://api.opensuse.org' */ -function getServiceClassAuthOrigin(serviceClass, authOverride) { +function getServiceClassAuthOrigin(serviceClass, authOverride, configOverride) { if ( !serviceClass || !serviceClass.prototype || @@ -178,7 +180,8 @@ function getServiceClassAuthOrigin(serviceClass, authOverride) { if (auth.authorizedOrigins) { return serviceClass.auth.authorizedOrigins } else { - return [config.public.services[auth.serviceKey].authorizedOrigins] + const mergedConfig = _.merge(runnerConfig, configOverride) + return [mergedConfig.public.services[auth.serviceKey].authorizedOrigins] } } @@ -212,6 +215,7 @@ function fakeJwtToken() { * @param {string} options.jwtLoginEndpoint - jwtAuth Login endpoint. * @param {object} options.exampleOverride - Override example params in test. * @param {object} options.authOverride - Override class auth params. + * @param {object} options.configOverride - Override the config for this test. * @throws {TypeError} - Throws a TypeError if the input `serviceClass` is not an instance of BaseService, * or if `serviceClass` is missing authorizedOrigins. * @@ -237,6 +241,7 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { jwtLoginEndpoint, exampleOverride = {}, authOverride, + configOverride, } = options if (contentType && typeof contentType !== 'string') { throw new TypeError('Invalid contentType: Must be a String.') @@ -254,11 +259,18 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { if (authOverride && typeof authOverride !== 'object') { throw new TypeError('Invalid authOverride: Must be an Object.') } + if (configOverride && typeof configOverride !== 'object') { + throw new TypeError('Invalid configOverride: Must be an Object.') + } const auth = { ...serviceClass.auth, ...authOverride } const fakeUser = auth.userKey ? 'fake-user' : undefined const fakeSecret = 'fake-secret' - const authOrigins = getServiceClassAuthOrigin(serviceClass, authOverride) + const authOrigins = getServiceClassAuthOrigin( + serviceClass, + authOverride, + configOverride, + ) const config = generateFakeConfig( serviceClass, fakeSecret, @@ -344,10 +356,14 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { }) expect( - await serviceClass.invoke(defaultContext, config, { - ...exampleInvokeParams, - ...exampleOverride, - }), + await serviceClass.invoke( + defaultContext, + _.merge(config, configOverride), + { + ...exampleInvokeParams, + ...exampleOverride, + }, + ), ).to.not.have.property('isError') // if we get 'Mocks not yet satisfied' we have redundent authOrigins or we are missing a critical request From cf34fae5b9c1ea88e858e87989843626b45d42d2 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sun, 25 Feb 2024 00:34:39 +0200 Subject: [PATCH 28/50] Fix example params by split into path and query Splits testAuth example params into path and query params. Introduce paramType to getBadgeExampleCall --- services/test-helpers.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/services/test-helpers.js b/services/test-helpers.js index 3d18480e9cf58..a892b57f6a9af 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -38,6 +38,7 @@ function noToken(serviceClass) { * Retrieves an example set of parameters for invoking a service class using OpenAPI example of that class. * * @param {BaseService} serviceClass The service class containing OpenAPI specifications. + * @param {'path'|'query'} paramType The type of params to extract, may be path params or query params. * @returns {object} An object with call params to use with a service invoke of the first OpenAPI example. * @throws {TypeError} - Throws a TypeError if the input `serviceClass` is not an instance of BaseService, * or if it lacks the expected structure. @@ -49,7 +50,7 @@ function noToken(serviceClass) { * // Output: { stackexchangesite: 'stackoverflow', query: '123' } * StackExchangeReputation.invoke(defaultContext, config, example) */ -function getBadgeExampleCall(serviceClass) { +function getBadgeExampleCall(serviceClass, paramType) { if (!(serviceClass.prototype instanceof BaseService)) { throw new TypeError( 'Invalid serviceClass: Must be an instance of BaseService.', @@ -61,6 +62,9 @@ function getBadgeExampleCall(serviceClass) { `Missing OpenAPI in service class ${serviceClass.name}.`, ) } + if (!['path', 'query'].includes(paramType)) { + throw new TypeError('Invalid paramType: Must be path or query.') + } const firstOpenapiPath = Object.keys(serviceClass.openApi)[0] @@ -74,7 +78,9 @@ function getBadgeExampleCall(serviceClass) { // reformat structure for serviceClass.invoke const exampleInvokeParams = firstOpenapiExampleParams.reduce((acc, obj) => { - acc[obj.name] = obj.example + if (obj.in === paramType) { + acc[obj.name] = obj.example + } return acc }, {}) @@ -278,7 +284,8 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { authOrigins, authOverride, ) - const exampleInvokeParams = getBadgeExampleCall(serviceClass) + const exampleInvokePathParams = getBadgeExampleCall(serviceClass, 'path') + const exampleInvokeQueryParams = getBadgeExampleCall(serviceClass, 'query') if (options && typeof options !== 'object') { throw new TypeError('Invalid options: Must be an object.') } @@ -360,7 +367,11 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { defaultContext, _.merge(config, configOverride), { - ...exampleInvokeParams, + ...exampleInvokePathParams, + ...exampleOverride, + }, + { + ...exampleInvokeQueryParams, ...exampleOverride, }, ), From 8adaf3d2f3365d4f290256a04a87ad847e9ea40a Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Fri, 23 Feb 2024 22:10:46 +0200 Subject: [PATCH 29/50] Refactor BitbucketPullRequest for testAuth Refactor to add auth static properties. for better integration with testAuth. Refactor bitbucketpullrequest for testAuth --- config/custom-environment-variables.yml | 2 ++ config/default.yml | 2 ++ core/server/server.js | 1 + .../bitbucket-pull-request.service.js | 34 +++++++++++-------- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/config/custom-environment-variables.yml b/config/custom-environment-variables.yml index 56919b467f1a4..315f94127e2d3 100644 --- a/config/custom-environment-variables.yml +++ b/config/custom-environment-variables.yml @@ -31,6 +31,8 @@ public: __format: 'json' services: + bitbucket: + authorizedOrigins: 'BITBUCKET_ORIGINS' bitbucketServer: authorizedOrigins: 'BITBUCKET_SERVER_ORIGINS' drone: diff --git a/config/default.yml b/config/default.yml index 5a6fb10393486..b45208dd1055d 100644 --- a/config/default.yml +++ b/config/default.yml @@ -14,6 +14,8 @@ public: cors: allowedOrigin: [] services: + bitbucket: + authorizedOrigins: 'https://bitbucket.org' github: baseUri: 'https://api.github.com' debug: diff --git a/core/server/server.js b/core/server/server.js index d6c6256a1165e..4a220a43c1bac 100644 --- a/core/server/server.js +++ b/core/server/server.js @@ -118,6 +118,7 @@ const publicConfigSchema = Joi.object({ allowedOrigin: Joi.array().items(optionalUrl).required(), }, services: Joi.object({ + bitbucket: defaultService, bitbucketServer: defaultService, drone: defaultService, github: { diff --git a/services/bitbucket/bitbucket-pull-request.service.js b/services/bitbucket/bitbucket-pull-request.service.js index 0e89e1d79c211..8eb142f188612 100644 --- a/services/bitbucket/bitbucket-pull-request.service.js +++ b/services/bitbucket/bitbucket-pull-request.service.js @@ -32,6 +32,20 @@ function pullRequestClassGenerator(raw) { queryParamSchema, } + static auth = { + userKey: 'bitbucket_username', + passKey: 'bitbucket_password', + serviceKey: 'bitbucket', + isRequired: true, + } + + static authServer = { + userKey: 'bitbucket_server_username', + passKey: 'bitbucket_server_password', + serviceKey: 'bitbucketServer', + isRequired: true, + } + static get openApi() { const key = `/bitbucket/${routePrefix}/{user}/{repo}` const route = {} @@ -71,27 +85,16 @@ function pullRequestClassGenerator(raw) { constructor(context, config) { super(context, config) - this.bitbucketAuthHelper = new AuthHelper( - { - userKey: 'bitbucket_username', - passKey: 'bitbucket_password', - authorizedOrigins: ['https://bitbucket.org'], - }, - config, - ) + // can only be set here as we must get config this.bitbucketServerAuthHelper = new AuthHelper( - { - userKey: 'bitbucket_server_username', - passKey: 'bitbucket_server_password', - serviceKey: 'bitbucketServer', - }, + BitbucketPullRequest.authServer, config, ) } async fetchCloud({ user, repo }) { return this._requestJson( - this.bitbucketAuthHelper.withBasicAuth({ + this.authHelper.withBasicAuth({ url: `https://bitbucket.org/api/2.0/repositories/${user}/${repo}/pullrequests/`, schema, options: { searchParams: { state: 'OPEN', limit: 0 } }, @@ -103,7 +106,7 @@ function pullRequestClassGenerator(raw) { // https://docs.atlassian.com/bitbucket-server/rest/5.16.0/bitbucket-rest.html#idm46229602363312 async fetchServer({ server, user, repo }) { return this._requestJson( - this.bitbucketServerAuthHelper.withBasicAuth({ + this.authHelper.withBasicAuth({ url: `${server}/rest/api/1.0/projects/${user}/repos/${repo}/pull-requests`, schema, options: { @@ -121,6 +124,7 @@ function pullRequestClassGenerator(raw) { async fetch({ server, user, repo }) { if (server !== undefined) { + this.authHelper = this.bitbucketServerAuthHelper return this.fetchServer({ server, user, repo }) } else { return this.fetchCloud({ user, repo }) From 988290d587e8c197061f94bd6f302dc3a5b893ae Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Thu, 22 Feb 2024 23:08:37 +0200 Subject: [PATCH 30/50] Refactor: use testAuth for BitbucketPullRequest --- .../bitbucket/bitbucket-pull-request.spec.js | 96 ++++++------------- 1 file changed, 31 insertions(+), 65 deletions(-) diff --git a/services/bitbucket/bitbucket-pull-request.spec.js b/services/bitbucket/bitbucket-pull-request.spec.js index ff504d1cd428b..e8d622b313a83 100644 --- a/services/bitbucket/bitbucket-pull-request.spec.js +++ b/services/bitbucket/bitbucket-pull-request.spec.js @@ -1,73 +1,39 @@ -import { expect } from 'chai' -import nock from 'nock' -import { cleanUpNockAfterEach, defaultContext } from '../test-helpers.js' -import { BitbucketRawPullRequests } from './bitbucket-pull-request.service.js' - -describe('BitbucketPullRequest', function () { - cleanUpNockAfterEach() - - const user = 'admin' - const pass = 'password' - - it('Sends auth headers to Bitbucket as configured', async function () { - const scope = nock('https://bitbucket.org/api/2.0/repositories/') - .get(/.*/) - .basicAuth({ user, pass }) - .reply(200, { size: 42 }) - - expect( - await BitbucketRawPullRequests.invoke( - defaultContext, - { - public: { - services: { - bitbucketServer: { - authorizedOrigins: [], - }, - }, - }, - private: { bitbucket_username: user, bitbucket_password: pass }, - }, - { user: 'shields-io', repo: 'test-repo' }, - ), - ).to.deep.equal({ - message: '42', - color: 'yellow', +import { testAuth } from '../test-helpers.js' +import { + BitbucketRawPullRequests, + BitbucketNonRawPullRequests, +} from './bitbucket-pull-request.service.js' + +describe('BitbucketRawPullRequests', function () { + describe('auth', function () { + it('sends the auth information to Bitbucket cloud as configured', async function () { + return testAuth( + BitbucketRawPullRequests, + 'BasicAuth', + { size: 42 }, + { exampleOverride: { server: undefined } }, + ) }) - scope.done() + it('sends the auth information to Bitbucket instence as configured', async function () { + return testAuth(BitbucketRawPullRequests, 'BasicAuth', { size: 42 }) + }) }) +}) - it('Sends auth headers to Bitbucket Server as configured', async function () { - const scope = nock('https://bitbucket.example.test/rest/api/1.0/projects') - .get(/.*/) - .basicAuth({ user, pass }) - .reply(200, { size: 42 }) - - expect( - await BitbucketRawPullRequests.invoke( - defaultContext, - { - public: { - services: { - bitbucketServer: { - authorizedOrigins: ['https://bitbucket.example.test'], - }, - }, - }, - private: { - bitbucket_server_username: user, - bitbucket_server_password: pass, - }, - }, - { user: 'project', repo: 'repo' }, - { server: 'https://bitbucket.example.test' }, - ), - ).to.deep.equal({ - message: '42', - color: 'yellow', +describe('BitbucketNonRawPullRequests', function () { + describe('auth', function () { + it('sends the auth information to Bitbucket cloud as configured', async function () { + return testAuth( + BitbucketNonRawPullRequests, + 'BasicAuth', + { size: 42 }, + { exampleOverride: { server: undefined } }, + ) }) - scope.done() + it('sends the auth information to Bitbucket instence as configured', async function () { + return testAuth(BitbucketNonRawPullRequests, 'BasicAuth', { size: 42 }) + }) }) }) From b9d51d1aba4c4e9db149a4e975ce51ddef4a6a05 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Fri, 23 Feb 2024 22:35:43 +0200 Subject: [PATCH 31/50] Fix BitbucketPullRequest tests using authOverride --- .../bitbucket/bitbucket-pull-request.spec.js | 60 +++++++++++++++++-- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/services/bitbucket/bitbucket-pull-request.spec.js b/services/bitbucket/bitbucket-pull-request.spec.js index e8d622b313a83..5a6622f264379 100644 --- a/services/bitbucket/bitbucket-pull-request.spec.js +++ b/services/bitbucket/bitbucket-pull-request.spec.js @@ -4,6 +4,36 @@ import { BitbucketNonRawPullRequests, } from './bitbucket-pull-request.service.js' +const serverConfigOverride = { + public: { + services: { + bitbucketServer: { + authorizedOrigins: ['https://bitbucket.mydomain.net'], + }, + bitbucket: { + authorizedOrigins: ['https://bitbucket.org'], + }, + }, + }, + private: { + bitbucket_username: 'must-be-set-for-class-constructor', + bitbucket_password: 'must-be-set-for-class-constructor', + }, +} + +const cloudConfigOverride = { + public: { + services: { + bitbucket: { + authorizedOrigins: ['https://bitbucket.org'], + }, + bitbucketServer: { + authorizedOrigins: [], + }, + }, + }, +} + describe('BitbucketRawPullRequests', function () { describe('auth', function () { it('sends the auth information to Bitbucket cloud as configured', async function () { @@ -11,12 +41,23 @@ describe('BitbucketRawPullRequests', function () { BitbucketRawPullRequests, 'BasicAuth', { size: 42 }, - { exampleOverride: { server: undefined } }, + { + exampleOverride: { server: undefined }, + configOverride: cloudConfigOverride, + }, ) }) it('sends the auth information to Bitbucket instence as configured', async function () { - return testAuth(BitbucketRawPullRequests, 'BasicAuth', { size: 42 }) + return testAuth( + BitbucketRawPullRequests, + 'BasicAuth', + { size: 42 }, + { + authOverride: BitbucketRawPullRequests.authServer, + configOverride: serverConfigOverride, + }, + ) }) }) }) @@ -28,12 +69,23 @@ describe('BitbucketNonRawPullRequests', function () { BitbucketNonRawPullRequests, 'BasicAuth', { size: 42 }, - { exampleOverride: { server: undefined } }, + { + exampleOverride: { server: undefined }, + configOverride: cloudConfigOverride, + }, ) }) it('sends the auth information to Bitbucket instence as configured', async function () { - return testAuth(BitbucketNonRawPullRequests, 'BasicAuth', { size: 42 }) + return testAuth( + BitbucketNonRawPullRequests, + 'BasicAuth', + { size: 42 }, + { + authOverride: BitbucketNonRawPullRequests.authServer, + configOverride: serverConfigOverride, + }, + ) }) }) }) From b23e3d41737e4a547ca4eca9ea4d55c7078fc910 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sun, 25 Feb 2024 23:40:36 +0200 Subject: [PATCH 32/50] Add auth testing for jenkins services with testAuth --- services/jenkins/jenkins-build.spec.js | 62 +++++++---------------- services/jenkins/jenkins-coverage.spec.js | 25 +++++++++ services/jenkins/jenkins-tests.spec.js | 28 ++++++++++ 3 files changed, 70 insertions(+), 45 deletions(-) create mode 100644 services/jenkins/jenkins-coverage.spec.js create mode 100644 services/jenkins/jenkins-tests.spec.js diff --git a/services/jenkins/jenkins-build.spec.js b/services/jenkins/jenkins-build.spec.js index 4bba9b389fd68..ad84c3fc912bd 100644 --- a/services/jenkins/jenkins-build.spec.js +++ b/services/jenkins/jenkins-build.spec.js @@ -1,10 +1,18 @@ -import { expect } from 'chai' -import nock from 'nock' import { test, forCases, given } from 'sazerac' import { renderBuildStatusBadge } from '../build-status.js' -import { cleanUpNockAfterEach, defaultContext } from '../test-helpers.js' +import { testAuth } from '../test-helpers.js' import JenkinsBuild from './jenkins-build.service.js' +const authConfigOverride = { + public: { + services: { + jenkins: { + authorizedOrigins: ['https://ci.eclipse.org'], + }, + }, + }, +} + describe('JenkinsBuild', function () { test(JenkinsBuild.prototype.transform, () => { forCases([ @@ -57,49 +65,13 @@ describe('JenkinsBuild', function () { }) describe('auth', function () { - cleanUpNockAfterEach() - - const user = 'admin' - const pass = 'password' - const config = { - public: { - services: { - jenkins: { - authorizedOrigins: ['https://jenkins.ubuntu.com'], - }, - }, - }, - private: { - jenkins_user: user, - jenkins_pass: pass, - }, - } - it('sends the auth information as configured', async function () { - const scope = nock('https://jenkins.ubuntu.com') - .get('/server/job/curtin-vmtest-daily-x/api/json?tree=color') - // This ensures that the expected credentials are actually being sent with the HTTP request. - // Without this the request wouldn't match and the test would fail. - .basicAuth({ user, pass }) - .reply(200, { color: 'blue' }) - - expect( - await JenkinsBuild.invoke( - defaultContext, - config, - {}, - { - jobUrl: - 'https://jenkins.ubuntu.com/server/job/curtin-vmtest-daily-x', - }, - ), - ).to.deep.equal({ - label: undefined, - message: 'passing', - color: 'brightgreen', - }) - - scope.done() + return testAuth( + JenkinsBuild, + 'BasicAuth', + { color: 'blue' }, + { configOverride: authConfigOverride }, + ) }) }) }) diff --git a/services/jenkins/jenkins-coverage.spec.js b/services/jenkins/jenkins-coverage.spec.js new file mode 100644 index 0000000000000..653ed202fc477 --- /dev/null +++ b/services/jenkins/jenkins-coverage.spec.js @@ -0,0 +1,25 @@ +import { testAuth } from '../test-helpers.js' +import JenkinsCoverage from './jenkins-coverage.service.js' + +const authConfigOverride = { + public: { + services: { + jenkins: { + authorizedOrigins: ['https://jenkins.sqlalchemy.org'], + }, + }, + }, +} + +describe('JenkinsCoverage', function () { + describe('auth', function () { + it('sends the auth information as configured', async function () { + return testAuth( + JenkinsCoverage, + 'BasicAuth', + { results: { elements: [{ name: 'Lines', ratio: 88 }] } }, + { configOverride: authConfigOverride }, + ) + }) + }) +}) diff --git a/services/jenkins/jenkins-tests.spec.js b/services/jenkins/jenkins-tests.spec.js new file mode 100644 index 0000000000000..f2404f2005013 --- /dev/null +++ b/services/jenkins/jenkins-tests.spec.js @@ -0,0 +1,28 @@ +import { testAuth } from '../test-helpers.js' +import JenkinsTests from './jenkins-tests.service.js' + +const authConfigOverride = { + public: { + services: { + jenkins: { + authorizedOrigins: ['https://jenkins.sqlalchemy.org'], + }, + }, + }, +} + +describe('JenkinsTests', function () { + describe('auth', function () { + it('sends the auth information as configured', async function () { + return testAuth( + JenkinsTests, + 'BasicAuth', + { actions: [{ totalCount: 3, failCount: 2, skipCount: 1 }] }, + { + configOverride: authConfigOverride, + exampleOverride: { compact_message: '' }, + }, + ) + }) + }) +}) From 84bee8023f21cfd0f0bc4d6caa8c01cd955ad5f6 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sat, 2 Mar 2024 13:32:15 +0200 Subject: [PATCH 33/50] Add auth testing for Jira services with testAuth --- services/jira/jira-issue.spec.js | 41 ++++++++-------------- services/jira/jira-sprint.spec.js | 55 ++++++++---------------------- services/jira/jira-test-helpers.js | 8 ++--- 3 files changed, 30 insertions(+), 74 deletions(-) diff --git a/services/jira/jira-issue.spec.js b/services/jira/jira-issue.spec.js index 4d3cbfdd3911f..b1e23d30ce87e 100644 --- a/services/jira/jira-issue.spec.js +++ b/services/jira/jira-issue.spec.js @@ -1,35 +1,22 @@ -import { expect } from 'chai' -import nock from 'nock' -import { cleanUpNockAfterEach, defaultContext } from '../test-helpers.js' +import { testAuth } from '../test-helpers.js' import JiraIssue from './jira-issue.service.js' -import { user, pass, host, config } from './jira-test-helpers.js' +import { config } from './jira-test-helpers.js' describe('JiraIssue', function () { - cleanUpNockAfterEach() - - it('sends the auth information as configured', async function () { - const scope = nock(`https://${host}`) - .get(`/rest/api/2/issue/${encodeURIComponent('secure-234')}`) - // This ensures that the expected credentials are actually being sent with the HTTP request. - // Without this the request wouldn't match and the test would fail. - .basicAuth({ user, pass }) - .reply(200, { fields: { status: { name: 'in progress' } } }) - - expect( - await JiraIssue.invoke( - defaultContext, - config, + describe('auth', function () { + it('sends the auth information as configured', async function () { + testAuth( + JiraIssue, + 'BasicAuth', { - issueKey: 'secure-234', + fields: { + status: { + name: 'in progress', + }, + }, }, - { baseUrl: `https://${host}` }, - ), - ).to.deep.equal({ - label: 'secure-234', - message: 'in progress', - color: 'lightgrey', + { configOverride: config }, + ) }) - - scope.done() }) }) diff --git a/services/jira/jira-sprint.spec.js b/services/jira/jira-sprint.spec.js index 33a647ed7c371..e87fcafbcd32b 100644 --- a/services/jira/jira-sprint.spec.js +++ b/services/jira/jira-sprint.spec.js @@ -1,49 +1,22 @@ -import { expect } from 'chai' -import nock from 'nock' -import { cleanUpNockAfterEach, defaultContext } from '../test-helpers.js' +import { testAuth } from '../test-helpers.js' import JiraSprint from './jira-sprint.service.js' -import { - user, - pass, - host, - config, - sprintId, - sprintQueryString, -} from './jira-test-helpers.js' +import { config } from './jira-test-helpers.js' describe('JiraSprint', function () { - cleanUpNockAfterEach() - - it('sends the auth information as configured', async function () { - const scope = nock(`https://${host}`) - .get('/jira/rest/api/2/search') - .query(sprintQueryString) - // This ensures that the expected credentials are actually being sent with the HTTP request. - // Without this the request wouldn't match and the test would fail. - .basicAuth({ user, pass }) - .reply(200, { - total: 2, - issues: [ - { fields: { resolution: { name: 'done' } } }, - { fields: { resolution: { name: 'Unresolved' } } }, - ], - }) - - expect( - await JiraSprint.invoke( - defaultContext, - config, + describe('auth', function () { + it('sends the auth information as configured', async function () { + testAuth( + JiraSprint, + 'BasicAuth', { - sprintId, + total: 2, + issues: [ + { fields: { resolution: { name: 'done' } } }, + { fields: { resolution: { name: 'Unresolved' } } }, + ], }, - { baseUrl: `https://${host}/jira` }, - ), - ).to.deep.equal({ - label: 'completion', - message: '50%', - color: 'orange', + { configOverride: config }, + ) }) - - scope.done() }) }) diff --git a/services/jira/jira-test-helpers.js b/services/jira/jira-test-helpers.js index e188179146bc5..6cdcdfff8919f 100644 --- a/services/jira/jira-test-helpers.js +++ b/services/jira/jira-test-helpers.js @@ -5,18 +5,14 @@ const sprintQueryString = { maxResults: 500, } -const user = 'admin' -const pass = 'password' -const host = 'myprivatejira.test' const config = { public: { services: { jira: { - authorizedOrigins: [`https://${host}`], + authorizedOrigins: ['https://issues.apache.org'], }, }, }, - private: { jira_user: user, jira_pass: pass }, } -export { sprintId, sprintQueryString, user, pass, host, config } +export { sprintId, sprintQueryString, config } From 1c15782ce7570338d4c605d0b4602ffe4174ee1c Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sat, 2 Mar 2024 17:26:00 +0200 Subject: [PATCH 34/50] Add auth testing for Nexus services with testAuth --- services/nexus/nexus.spec.js | 50 +++++++++--------------------------- 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/services/nexus/nexus.spec.js b/services/nexus/nexus.spec.js index 1f4114bfdc61a..91f6ccbb6b4a6 100644 --- a/services/nexus/nexus.spec.js +++ b/services/nexus/nexus.spec.js @@ -1,6 +1,5 @@ import { expect } from 'chai' -import nock from 'nock' -import { cleanUpNockAfterEach, defaultContext } from '../test-helpers.js' +import { testAuth } from '../test-helpers.js' import { InvalidResponse, NotFound } from '../index.js' import Nexus from './nexus.service.js' @@ -113,52 +112,27 @@ describe('Nexus', function () { }) describe('auth', function () { - cleanUpNockAfterEach() - - const user = 'admin' - const pass = 'password' const config = { public: { services: { nexus: { - authorizedOrigins: ['https://repository.jboss.org'], + authorizedOrigins: ['https://oss.sonatype.org'], }, }, }, - private: { - nexus_user: user, - nexus_pass: pass, - }, } - it('sends the auth information as configured', async function () { - const scope = nock('https://repository.jboss.org') - .get('/nexus/service/local/lucene/search') - .query({ g: 'jboss', a: 'jboss-client' }) - // This ensures that the expected credentials are actually being sent with the HTTP request. - // Without this the request wouldn't match and the test would fail. - .basicAuth({ user, pass }) - .reply(200, { data: [{ latestRelease: '2.3.4' }] }) - - expect( - await Nexus.invoke( - defaultContext, - config, - { - repo: 'r', - groupId: 'jboss', - artifactId: 'jboss-client', + testAuth( + Nexus, + 'BasicAuth', + { + data: { + baseVersion: '9.3.95', + version: '9.3.95', }, - { - server: 'https://repository.jboss.org/nexus', - }, - ), - ).to.deep.equal({ - message: 'v2.3.4', - color: 'blue', - }) - - scope.done() + }, + { configOverride: config }, + ) }) }) }) From a713669ee8c30c2f239c9c8a74e3175f2549b1a6 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sat, 9 Mar 2024 12:40:22 +0200 Subject: [PATCH 35/50] Improve error handling in getServiceClassAuthOrigin Enhance reliability by throwing a TypeError when a service key definition is missing. It's common for a key to be missing and might require the developer that writes tests to provide a fake on that fits the examples used. Key changes: - Added a check for missing service key definitions in getServiceClassAuthOrigin. - Throws a TypeError with a clear message if the key is not found. - Guides devs to use an override if needed which is often. --- services/test-helpers.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/test-helpers.js b/services/test-helpers.js index a892b57f6a9af..34fefdbd5f07d 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -187,6 +187,11 @@ function getServiceClassAuthOrigin(serviceClass, authOverride, configOverride) { return serviceClass.auth.authorizedOrigins } else { const mergedConfig = _.merge(runnerConfig, configOverride) + if (!mergedConfig.public.services[auth.serviceKey]) { + throw new TypeError( + `Missing service key defenition for ${auth.serviceKey}: Use an override if applicable.`, + ) + } return [mergedConfig.public.services[auth.serviceKey].authorizedOrigins] } } From 99a01e148e1120d2fba836aaab553a2f8de94eb3 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sat, 9 Mar 2024 12:48:55 +0200 Subject: [PATCH 36/50] Fix edge case in testAuth for user only auth Addresses a scenario where generateFakeConfig/testAuth would fail for classes using auth.userKey without auth.passKey. Ensures proper handling of user-specific authentication even if a password key is not present. Throws a TypeError only when both fakeKey and fakeUser are missing. Better fit for AuthHelper behivor. --- services/test-helpers.js | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/services/test-helpers.js b/services/test-helpers.js index 34fefdbd5f07d..d9677e1b46e42 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -115,8 +115,8 @@ function generateFakeConfig( 'Invalid serviceClass: Must be an instance of BaseService.', ) } - if (!fakeKey || typeof fakeKey !== 'string') { - throw new TypeError('Invalid fakeKey: Must be a String.') + if (!fakeKey && !fakeUser) { + throw new TypeError('Must provide at least one: fakeKey or fakeUser.') } if (!fakeauthorizedOrigins || !Array.isArray(fakeauthorizedOrigins)) { throw new TypeError('Invalid fakeauthorizedOrigins: Must be an array.') @@ -126,21 +126,21 @@ function generateFakeConfig( if (Object.keys(auth).length === 0) { throw new Error(`Auth empty for ${serviceClass.name}.`) } - if (!auth.passKey) { + if (fakeKey && !auth.passKey) { + throw new Error(`Missing auth.passKey for ${serviceClass.name}.`) + } + if (fakeKey && typeof fakeKey !== 'string') { throw new Error(`Missing auth.passKey for ${serviceClass.name}.`) } // Extract the passKey property from auth, or use a default if not present - const passKeyProperty = auth.passKey - let passUserProperty = 'placeholder' - if (fakeUser) { - if (typeof fakeKey !== 'string') { - throw new TypeError('Invalid fakeUser: Must be a String.') - } - if (!auth.userKey) { - throw new Error(`Missing auth.userKey for ${serviceClass.name}.`) - } - passUserProperty = auth.userKey + const passKeyProperty = auth.passKey ? auth.passKey : undefined + if (fakeUser && typeof fakeUser !== 'string') { + throw new TypeError('Invalid fakeUser: Must be a String.') } + if (fakeUser && !auth.userKey) { + throw new Error(`Missing auth.userKey for ${serviceClass.name}.`) + } + const passUserProperty = auth.userKey ? auth.userKey : undefined // Build and return the configuration object with the fake key return { @@ -276,7 +276,12 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { const auth = { ...serviceClass.auth, ...authOverride } const fakeUser = auth.userKey ? 'fake-user' : undefined - const fakeSecret = 'fake-secret' + const fakeSecret = auth.passKey ? 'fake-secret' : undefined + if (!fakeUser && !fakeSecret) { + throw new TypeError( + `Missing auth pass/user for ${serviceClass.name}. At least one is required.`, + ) + } const authOrigins = getServiceClassAuthOrigin( serviceClass, authOverride, From 74554d74a731a6431903d8e970de607f09ea1453 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sat, 9 Mar 2024 17:20:04 +0200 Subject: [PATCH 37/50] Add option to not import openApi example in testAuth Added for services without openApi examples. Dev can use example override to add example inputs in those cases. --- services/test-helpers.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/services/test-helpers.js b/services/test-helpers.js index d9677e1b46e42..a8944e5dc1556 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -227,6 +227,7 @@ function fakeJwtToken() { * @param {object} options.exampleOverride - Override example params in test. * @param {object} options.authOverride - Override class auth params. * @param {object} options.configOverride - Override the config for this test. + * @param {boolean} options.ignoreOpenApiExample - For classes without OpenApi example ignore for usage of override examples only * @throws {TypeError} - Throws a TypeError if the input `serviceClass` is not an instance of BaseService, * or if `serviceClass` is missing authorizedOrigins. * @@ -253,6 +254,7 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { exampleOverride = {}, authOverride, configOverride, + ignoreOpenApiExample = false, } = options if (contentType && typeof contentType !== 'string') { throw new TypeError('Invalid contentType: Must be a String.') @@ -273,6 +275,9 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { if (configOverride && typeof configOverride !== 'object') { throw new TypeError('Invalid configOverride: Must be an Object.') } + if (ignoreOpenApiExample && typeof ignoreOpenApiExample !== 'boolean') { + throw new TypeError('Invalid ignoreOpenApiExample: Must be an Object.') + } const auth = { ...serviceClass.auth, ...authOverride } const fakeUser = auth.userKey ? 'fake-user' : undefined @@ -294,8 +299,12 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { authOrigins, authOverride, ) - const exampleInvokePathParams = getBadgeExampleCall(serviceClass, 'path') - const exampleInvokeQueryParams = getBadgeExampleCall(serviceClass, 'query') + const exampleInvokePathParams = ignoreOpenApiExample + ? undefined + : getBadgeExampleCall(serviceClass, 'path') + const exampleInvokeQueryParams = ignoreOpenApiExample + ? undefined + : getBadgeExampleCall(serviceClass, 'query') if (options && typeof options !== 'object') { throw new TypeError('Invalid options: Must be an object.') } From 8b8bf19b57058e73a045b8f6c74a504fcf297518 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sat, 9 Mar 2024 17:20:59 +0200 Subject: [PATCH 38/50] Add auth testing for Sonar services with testAuth --- services/sonar/sonar-coverage.spec.js | 19 ++++++ .../sonar-documented-api-density.spec.js | 16 +++++ services/sonar/sonar-fortify-rating.spec.js | 58 +++++-------------- services/sonar/sonar-generic.spec.js | 25 ++++++++ services/sonar/sonar-quality-gate.spec.js | 16 +++++ services/sonar/sonar-spec-helpers.js | 36 ++++++++++++ services/sonar/sonar-tech-debt.spec.js | 16 +++++ services/sonar/sonar-tests.spec.js | 16 +++++ services/sonar/sonar-violations.spec.js | 19 ++++++ 9 files changed, 176 insertions(+), 45 deletions(-) create mode 100644 services/sonar/sonar-coverage.spec.js create mode 100644 services/sonar/sonar-generic.spec.js create mode 100644 services/sonar/sonar-spec-helpers.js diff --git a/services/sonar/sonar-coverage.spec.js b/services/sonar/sonar-coverage.spec.js new file mode 100644 index 0000000000000..c6c2e1e5086b2 --- /dev/null +++ b/services/sonar/sonar-coverage.spec.js @@ -0,0 +1,19 @@ +import { testAuth } from '../test-helpers.js' +import SonarCoverage from './sonar-coverage.service.js' +import { + legacySonarResponse, + testAuthConfigOverride, +} from './sonar-spec-helpers.js' + +describe('SonarCoverage', function () { + describe('auth', function () { + it('sends the auth information as configured', async function () { + return testAuth( + SonarCoverage, + 'BasicAuth', + legacySonarResponse('coverage', 95), + { configOverride: testAuthConfigOverride }, + ) + }) + }) +}) diff --git a/services/sonar/sonar-documented-api-density.spec.js b/services/sonar/sonar-documented-api-density.spec.js index a5ca4fe55267b..d8b01594bb1b6 100644 --- a/services/sonar/sonar-documented-api-density.spec.js +++ b/services/sonar/sonar-documented-api-density.spec.js @@ -1,5 +1,10 @@ import { test, given } from 'sazerac' +import { testAuth } from '../test-helpers.js' import SonarDocumentedApiDensity from './sonar-documented-api-density.service.js' +import { + legacySonarResponse, + testAuthConfigOverride, +} from './sonar-spec-helpers.js' describe('SonarDocumentedApiDensity', function () { test(SonarDocumentedApiDensity.render, () => { @@ -24,4 +29,15 @@ describe('SonarDocumentedApiDensity', function () { color: 'brightgreen', }) }) + + describe('auth', function () { + it('sends the auth information as configured', async function () { + return testAuth( + SonarDocumentedApiDensity, + 'BasicAuth', + legacySonarResponse('density', 93), + { configOverride: testAuthConfigOverride }, + ) + }) + }) }) diff --git a/services/sonar/sonar-fortify-rating.spec.js b/services/sonar/sonar-fortify-rating.spec.js index 4fa12729598b6..6538c473de996 100644 --- a/services/sonar/sonar-fortify-rating.spec.js +++ b/services/sonar/sonar-fortify-rating.spec.js @@ -1,51 +1,19 @@ -import { expect } from 'chai' -import nock from 'nock' -import { cleanUpNockAfterEach, defaultContext } from '../test-helpers.js' +import { testAuth } from '../test-helpers.js' import SonarFortifyRating from './sonar-fortify-rating.service.js' - -const token = 'abc123def456' -const config = { - public: { - services: { - sonar: { authorizedOrigins: ['http://sonar.petalslink.com'] }, - }, - }, - private: { - sonarqube_token: token, - }, -} +import { + legacySonarResponse, + testAuthConfigOverride, +} from './sonar-spec-helpers.js' describe('SonarFortifyRating', function () { - cleanUpNockAfterEach() - - it('sends the auth information as configured', async function () { - const scope = nock('http://sonar.petalslink.com') - .get('/api/measures/component') - .query({ - componentKey: 'org.ow2.petals:petals-se-ase', - metricKeys: 'fortify-security-rating', - }) - // This ensures that the expected credentials are actually being sent with the HTTP request. - // Without this the request wouldn't match and the test would fail. - .basicAuth({ user: token }) - .reply(200, { - component: { - measures: [{ metric: 'fortify-security-rating', value: 4 }], - }, - }) - - expect( - await SonarFortifyRating.invoke( - defaultContext, - config, - { component: 'org.ow2.petals:petals-se-ase' }, - { server: 'http://sonar.petalslink.com' }, - ), - ).to.deep.equal({ - color: 'green', - message: '4/5', + describe('auth', function () { + it('sends the auth information as configured', async function () { + testAuth( + SonarFortifyRating, + 'BasicAuth', + legacySonarResponse('fortify-security-rating', 4), + { configOverride: testAuthConfigOverride }, + ) }) - - scope.done() }) }) diff --git a/services/sonar/sonar-generic.spec.js b/services/sonar/sonar-generic.spec.js new file mode 100644 index 0000000000000..42c1910e0a071 --- /dev/null +++ b/services/sonar/sonar-generic.spec.js @@ -0,0 +1,25 @@ +import { testAuth } from '../test-helpers.js' +import SonarGeneric from './sonar-generic.service.js' +import { + legacySonarResponse, + testAuthConfigOverride, +} from './sonar-spec-helpers.js' + +describe('SonarGeneric', function () { + describe('auth', function () { + it('sends the auth information as configured', async function () { + testAuth(SonarGeneric, 'BasicAuth', legacySonarResponse('test', 903), { + configOverride: testAuthConfigOverride, + exampleOverride: { + component: 'test', + metricName: 'test', + branch: 'home', + server: + testAuthConfigOverride.public.services.sonar.authorizedOrigins[0], + sonarVersion: '4.2', + }, + ignoreOpenApiExample: true, + }) + }) + }) +}) diff --git a/services/sonar/sonar-quality-gate.spec.js b/services/sonar/sonar-quality-gate.spec.js index aa694ae066ba4..e9bc91fd28afd 100644 --- a/services/sonar/sonar-quality-gate.spec.js +++ b/services/sonar/sonar-quality-gate.spec.js @@ -1,5 +1,10 @@ import { test, given } from 'sazerac' +import { testAuth } from '../test-helpers.js' import SonarQualityGate from './sonar-quality-gate.service.js' +import { + legacySonarResponse, + testAuthConfigOverride, +} from './sonar-spec-helpers.js' describe('SonarQualityGate', function () { test(SonarQualityGate.render, () => { @@ -12,4 +17,15 @@ describe('SonarQualityGate', function () { color: 'critical', }) }) + + describe('auth', function () { + it('sends the auth information as configured', async function () { + return testAuth( + SonarQualityGate, + 'BasicAuth', + legacySonarResponse('alert_status', 'OK'), + { configOverride: testAuthConfigOverride }, + ) + }) + }) }) diff --git a/services/sonar/sonar-spec-helpers.js b/services/sonar/sonar-spec-helpers.js new file mode 100644 index 0000000000000..1d142f4800576 --- /dev/null +++ b/services/sonar/sonar-spec-helpers.js @@ -0,0 +1,36 @@ +import SonarBase from './sonar-base.js' +import { openApiQueryParams } from './sonar-helpers.js' + +const testAuthConfigOverride = { + public: { + services: { + [SonarBase.auth.serviceKey]: { + authorizedOrigins: [ + openApiQueryParams.find(v => v.name === 'server').example, + ], + }, + }, + }, +} + +/** + * Returns a legacy sonar api response with desired key and value + * + * @param {string} key Key for the response value + * @param {string|number} val Value to assign to response key + * @returns {object} Sonar api response + */ +function legacySonarResponse(key, val) { + return [ + { + msr: [ + { + key, + val, + }, + ], + }, + ] +} + +export { testAuthConfigOverride, legacySonarResponse } diff --git a/services/sonar/sonar-tech-debt.spec.js b/services/sonar/sonar-tech-debt.spec.js index b6ef2009205bd..a636f8c78facb 100644 --- a/services/sonar/sonar-tech-debt.spec.js +++ b/services/sonar/sonar-tech-debt.spec.js @@ -1,5 +1,10 @@ import { test, given } from 'sazerac' +import { testAuth } from '../test-helpers.js' import SonarTechDebt from './sonar-tech-debt.service.js' +import { + legacySonarResponse, + testAuthConfigOverride, +} from './sonar-spec-helpers.js' describe('SonarTechDebt', function () { test(SonarTechDebt.render, () => { @@ -29,4 +34,15 @@ describe('SonarTechDebt', function () { color: 'red', }) }) + + describe('auth', function () { + it('sends the auth information as configured', async function () { + return testAuth( + SonarTechDebt, + 'BasicAuth', + legacySonarResponse('sqale_debt_ratio', 95), + { configOverride: testAuthConfigOverride }, + ) + }) + }) }) diff --git a/services/sonar/sonar-tests.spec.js b/services/sonar/sonar-tests.spec.js index 81909602e18d3..9dd7681076bc1 100644 --- a/services/sonar/sonar-tests.spec.js +++ b/services/sonar/sonar-tests.spec.js @@ -1,5 +1,10 @@ import { test, given } from 'sazerac' +import { testAuth } from '../test-helpers.js' import { SonarTests } from './sonar-tests.service.js' +import { + legacySonarResponse, + testAuthConfigOverride, +} from './sonar-spec-helpers.js' describe('SonarTests', function () { test(SonarTests.render, () => { @@ -34,4 +39,15 @@ describe('SonarTests', function () { color: 'red', }) }) + + describe('auth', function () { + it('sends the auth information as configured', async function () { + return testAuth( + SonarTests, + 'BasicAuth', + legacySonarResponse('tests', 95), + { configOverride: testAuthConfigOverride }, + ) + }) + }) }) diff --git a/services/sonar/sonar-violations.spec.js b/services/sonar/sonar-violations.spec.js index 08aa8279125a9..2c24087279bc2 100644 --- a/services/sonar/sonar-violations.spec.js +++ b/services/sonar/sonar-violations.spec.js @@ -1,6 +1,11 @@ import { test, given } from 'sazerac' import { metric } from '../text-formatters.js' +import { testAuth } from '../test-helpers.js' import SonarViolations from './sonar-violations.service.js' +import { + legacySonarResponse, + testAuthConfigOverride, +} from './sonar-spec-helpers.js' describe('SonarViolations', function () { test(SonarViolations.render, () => { @@ -110,4 +115,18 @@ describe('SonarViolations', function () { color: 'red', }) }) + + describe('auth', function () { + it('sends the auth information as configured', async function () { + return testAuth( + SonarViolations, + 'BasicAuth', + legacySonarResponse('violations', 95), + { + configOverride: testAuthConfigOverride, + exampleOverride: { format: 'short' }, + }, + ) + }) + }) }) From a2b5331ea80f76cb98e93c56caad82299b8d2b06 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Fri, 22 Mar 2024 22:11:19 +0200 Subject: [PATCH 39/50] Add auth testing for SymfonyInsight services with testAuth --- services/symfony/symfony-insight-grade.spec.js | 18 ++++++++++++++++++ services/symfony/symfony-insight-stars.spec.js | 18 ++++++++++++++++++ .../symfony/symfony-insight-violations.spec.js | 17 +++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 services/symfony/symfony-insight-grade.spec.js create mode 100644 services/symfony/symfony-insight-stars.spec.js create mode 100644 services/symfony/symfony-insight-violations.spec.js diff --git a/services/symfony/symfony-insight-grade.spec.js b/services/symfony/symfony-insight-grade.spec.js new file mode 100644 index 0000000000000..3df9fb7bb3393 --- /dev/null +++ b/services/symfony/symfony-insight-grade.spec.js @@ -0,0 +1,18 @@ +import { testAuth } from '../test-helpers.js' +import SymfonyInsightGrade from './symfony-insight-grade.service.js' + +describe('SymfonyInsightGrade', function () { + describe('auth', function () { + it('sends the auth information as configured', async function () { + testAuth( + SymfonyInsightGrade, + 'BasicAuth', + ` + gold + finished + `, + { contentType: 'application/vnd.com.sensiolabs.insight+xml' }, + ) + }) + }) +}) diff --git a/services/symfony/symfony-insight-stars.spec.js b/services/symfony/symfony-insight-stars.spec.js new file mode 100644 index 0000000000000..039d479581929 --- /dev/null +++ b/services/symfony/symfony-insight-stars.spec.js @@ -0,0 +1,18 @@ +import { testAuth } from '../test-helpers.js' +import SymfonyInsightStars from './symfony-insight-stars.service.js' + +describe('SymfonyInsightStars', function () { + describe('auth', function () { + it('sends the auth information as configured', async function () { + testAuth( + SymfonyInsightStars, + 'BasicAuth', + ` + gold + finished + `, + { contentType: 'application/vnd.com.sensiolabs.insight+xml' }, + ) + }) + }) +}) diff --git a/services/symfony/symfony-insight-violations.spec.js b/services/symfony/symfony-insight-violations.spec.js new file mode 100644 index 0000000000000..bee2a5cd3c026 --- /dev/null +++ b/services/symfony/symfony-insight-violations.spec.js @@ -0,0 +1,17 @@ +import { testAuth } from '../test-helpers.js' +import SymfonyInsightViolations from './symfony-insight-violations.service.js' + +describe('SymfonyInsightViolations', function () { + describe('auth', function () { + it('sends the auth information as configured', async function () { + testAuth( + SymfonyInsightViolations, + 'BasicAuth', + ` + finished + `, + { contentType: 'application/vnd.com.sensiolabs.insight+xml' }, + ) + }) + }) +}) From 87c68e2bda5a733221261588be01090e124d5225 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Fri, 22 Mar 2024 22:56:34 +0200 Subject: [PATCH 40/50] Refactor TeamCity auth tests for use of testAuth --- services/teamcity/teamcity-build.spec.js | 43 +++++------------- services/teamcity/teamcity-coverage.spec.js | 49 ++++++--------------- services/teamcity/teamcity-test-helpers.js | 15 +++---- 3 files changed, 28 insertions(+), 79 deletions(-) diff --git a/services/teamcity/teamcity-build.spec.js b/services/teamcity/teamcity-build.spec.js index 1e207bce56370..2de605578fa6f 100644 --- a/services/teamcity/teamcity-build.spec.js +++ b/services/teamcity/teamcity-build.spec.js @@ -1,39 +1,16 @@ -import { expect } from 'chai' -import nock from 'nock' -import { cleanUpNockAfterEach, defaultContext } from '../test-helpers.js' +import { testAuth } from '../test-helpers.js' import TeamCityBuild from './teamcity-build.service.js' -import { user, pass, host, config } from './teamcity-test-helpers.js' +import { config } from './teamcity-test-helpers.js' describe('TeamCityBuild', function () { - cleanUpNockAfterEach() - - it('sends the auth information as configured', async function () { - const scope = nock(`https://${host}`) - .get(`/app/rest/builds/${encodeURIComponent('buildType:(id:bt678)')}`) - // This ensures that the expected credentials are actually being sent with the HTTP request. - // Without this the request wouldn't match and the test would fail. - .basicAuth({ user, pass }) - .reply(200, { - status: 'FAILURE', - statusText: - 'Tests failed: 1 (1 new), passed: 50246, ignored: 1, muted: 12', - }) - - expect( - await TeamCityBuild.invoke( - defaultContext, - config, - { - verbosity: 'e', - buildId: 'bt678', - }, - { server: `https://${host}` }, - ), - ).to.deep.equal({ - message: 'tests failed: 1 (1 new), passed: 50246, ignored: 1, muted: 12', - color: 'red', + describe('auth', function () { + it('sends the auth information as configured', async function () { + testAuth( + TeamCityBuild, + 'BasicAuth', + { status: 'SUCCESS', statusText: 'Success' }, + { configOverride: config }, + ) }) - - scope.done() }) }) diff --git a/services/teamcity/teamcity-coverage.spec.js b/services/teamcity/teamcity-coverage.spec.js index 0e7bbae64d3da..c1dcb8870551b 100644 --- a/services/teamcity/teamcity-coverage.spec.js +++ b/services/teamcity/teamcity-coverage.spec.js @@ -1,44 +1,21 @@ -import { expect } from 'chai' -import nock from 'nock' -import { cleanUpNockAfterEach, defaultContext } from '../test-helpers.js' +import { testAuth } from '../test-helpers.js' import TeamCityCoverage from './teamcity-coverage.service.js' -import { user, pass, host, config } from './teamcity-test-helpers.js' +import { config } from './teamcity-test-helpers.js' describe('TeamCityCoverage', function () { - cleanUpNockAfterEach() - - it('sends the auth information as configured', async function () { - const scope = nock(`https://${host}`) - .get( - `/app/rest/builds/${encodeURIComponent( - 'buildType:(id:bt678)', - )}/statistics`, - ) - .query({}) - // This ensures that the expected credentials are actually being sent with the HTTP request. - // Without this the request wouldn't match and the test would fail. - .basicAuth({ user, pass }) - .reply(200, { - property: [ - { name: 'CodeCoverageAbsSCovered', value: '82' }, - { name: 'CodeCoverageAbsSTotal', value: '100' }, - ], - }) - - expect( - await TeamCityCoverage.invoke( - defaultContext, - config, + describe('auth', function () { + it('sends the auth information as configured', async function () { + testAuth( + TeamCityCoverage, + 'BasicAuth', { - buildId: 'bt678', + property: [ + { name: 'CodeCoverageAbsSCovered', value: '93' }, + { name: 'CodeCoverageAbsSTotal', value: '95' }, + ], }, - { server: 'https://mycompany.teamcity.com' }, - ), - ).to.deep.equal({ - message: '82%', - color: 'yellowgreen', + { configOverride: config }, + ) }) - - scope.done() }) }) diff --git a/services/teamcity/teamcity-test-helpers.js b/services/teamcity/teamcity-test-helpers.js index 655a16cbd94fb..a3051df624a82 100644 --- a/services/teamcity/teamcity-test-helpers.js +++ b/services/teamcity/teamcity-test-helpers.js @@ -1,18 +1,13 @@ -const user = 'admin' -const pass = 'password' -const host = 'mycompany.teamcity.com' +import TeamCityBase from './teamcity-base.js' + const config = { public: { services: { - teamcity: { - authorizedOrigins: [`https://${host}`], + [TeamCityBase.auth.serviceKey]: { + authorizedOrigins: ['https://teamcity.jetbrains.com'], }, }, }, - private: { - teamcity_user: user, - teamcity_pass: pass, - }, } -export { user, pass, host, config } +export { config } From 8c38355fca6ddededa39820591b92444104d1ca9 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sun, 24 Mar 2024 22:58:37 +0200 Subject: [PATCH 41/50] Refactor testAuth function to handle defaultToEmptyStringForUser auth option --- services/test-helpers.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/test-helpers.js b/services/test-helpers.js index a8944e5dc1556..1640e12c7f3d6 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -280,7 +280,11 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { } const auth = { ...serviceClass.auth, ...authOverride } - const fakeUser = auth.userKey ? 'fake-user' : undefined + const fakeUser = auth.userKey + ? 'fake-user' + : auth.defaultToEmptyStringForUser + ? '' + : undefined const fakeSecret = auth.passKey ? 'fake-secret' : undefined if (!fakeUser && !fakeSecret) { throw new TypeError( From 57163eb4088b7110ef17a02acd82772056c76994 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sun, 24 Mar 2024 23:00:07 +0200 Subject: [PATCH 42/50] Add support for multiple requests in testAuth function Some services might have more then one request to auth with the server for the same shield. This commit adds support for those requests in testAuth using the new multipleRequests option. --- services/test-helpers.js | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/services/test-helpers.js b/services/test-helpers.js index 1640e12c7f3d6..b4654ef491339 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -228,6 +228,7 @@ function fakeJwtToken() { * @param {object} options.authOverride - Override class auth params. * @param {object} options.configOverride - Override the config for this test. * @param {boolean} options.ignoreOpenApiExample - For classes without OpenApi example ignore for usage of override examples only + * @param {boolean} options.multipleRequests - For classes that require multiple requests to complete the test. * @throws {TypeError} - Throws a TypeError if the input `serviceClass` is not an instance of BaseService, * or if `serviceClass` is missing authorizedOrigins. * @@ -242,8 +243,6 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { ) } - cleanUpNockAfterEach() - const { contentType, apiHeaderKey = 'x-api-key', @@ -255,6 +254,7 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { authOverride, configOverride, ignoreOpenApiExample = false, + multipleRequests = false, } = options if (contentType && typeof contentType !== 'string') { throw new TypeError('Invalid contentType: Must be a String.') @@ -278,6 +278,13 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { if (ignoreOpenApiExample && typeof ignoreOpenApiExample !== 'boolean') { throw new TypeError('Invalid ignoreOpenApiExample: Must be an Object.') } + if (multipleRequests && typeof multipleRequests !== 'boolean') { + throw new TypeError('Invalid multipleRequests: Must be an Object.') + } + + if (!multipleRequests) { + cleanUpNockAfterEach() + } const auth = { ...serviceClass.auth, ...authOverride } const fakeUser = auth.userKey @@ -321,6 +328,9 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { const scopeArr = [] authOrigins.forEach(authOrigin => { const scope = nock(authOrigin) + if (multipleRequests) { + scope.persist() + } scopeArr.push(scope) switch (authMethod) { case 'BasicAuth': @@ -400,6 +410,15 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { ), ).to.not.have.property('isError') + // cleapup persistance if we have multiple requests + if (multipleRequests) { + scopeArr.forEach(scope => scope.persist(false)) + nock.restore() + nock.cleanAll() + nock.enableNetConnect() + nock.activate() + } + // if we get 'Mocks not yet satisfied' we have redundent authOrigins or we are missing a critical request scopeArr.forEach(scope => scope.done()) } From 5dc774a0fb6761dcb4f436196ee1db110ea8baf4 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sun, 24 Mar 2024 23:01:46 +0200 Subject: [PATCH 43/50] Add auth testing for AzureDevOps services with testAuth --- .../azure-devops-coverage.spec.js | 33 +++++++++++++++++ .../azure-devops/azure-devops-tests.spec.js | 35 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 services/azure-devops/azure-devops-coverage.spec.js create mode 100644 services/azure-devops/azure-devops-tests.spec.js diff --git a/services/azure-devops/azure-devops-coverage.spec.js b/services/azure-devops/azure-devops-coverage.spec.js new file mode 100644 index 0000000000000..d820486173783 --- /dev/null +++ b/services/azure-devops/azure-devops-coverage.spec.js @@ -0,0 +1,33 @@ +import { testAuth } from '../test-helpers.js' +import AzureDevOpsCoverage from './azure-devops-coverage.service.js' + +describe('AzureDevOpsCoverage', function () { + describe('auth', function () { + it('sends the auth information as configured', async function () { + testAuth( + AzureDevOpsCoverage, + 'BasicAuth', + { + coverageData: [ + { + coverageStats: [ + { + label: 'Coverage', + total: 95, + covered: 93, + }, + ], + }, + ], + count: 1, + value: [ + { + id: 90395, + }, + ], + }, + { multipleRequests: true }, + ) + }) + }) +}) diff --git a/services/azure-devops/azure-devops-tests.spec.js b/services/azure-devops/azure-devops-tests.spec.js new file mode 100644 index 0000000000000..9bd757573802a --- /dev/null +++ b/services/azure-devops/azure-devops-tests.spec.js @@ -0,0 +1,35 @@ +import { testAuth } from '../test-helpers.js' +import AzureDevOpsTests from './azure-devops-tests.service.js' + +describe('AzureDevOpsTests', function () { + describe('auth', function () { + it('sends the auth information as configured', async function () { + testAuth( + AzureDevOpsTests, + 'BasicAuth', + { + aggregatedResultsAnalysis: { + totalTests: 95, + resultsByOutcome: { + Passed: { + count: 93, + }, + }, + }, + count: 1, + value: [ + { + id: 90395, + }, + ], + }, + { + exampleOverride: { + compact_message: undefined, + }, + multipleRequests: true, + }, + ) + }) + }) +}) From 1b425aa4e3141b14c7953de044819c633865e6e2 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sat, 6 Apr 2024 13:28:22 +0300 Subject: [PATCH 44/50] Fix async mocha tests Missing return for async mocha tests caused the tests to run async and throw errors of one test in another. This commit makes sure all testAuth tests return a promise to mocha for it to await. --- .../azure-devops-coverage.spec.js | 2 +- .../azure-devops/azure-devops-tests.spec.js | 2 +- services/jira/jira-issue.spec.js | 2 +- services/jira/jira-sprint.spec.js | 2 +- services/nexus/nexus.spec.js | 2 +- services/sonar/sonar-fortify-rating.spec.js | 2 +- services/sonar/sonar-generic.spec.js | 27 +++++++++++-------- .../symfony/symfony-insight-grade.spec.js | 2 +- .../symfony/symfony-insight-stars.spec.js | 2 +- .../symfony-insight-violations.spec.js | 2 +- services/teamcity/teamcity-build.spec.js | 2 +- services/teamcity/teamcity-coverage.spec.js | 2 +- 12 files changed, 27 insertions(+), 22 deletions(-) diff --git a/services/azure-devops/azure-devops-coverage.spec.js b/services/azure-devops/azure-devops-coverage.spec.js index d820486173783..9c05337bd0db0 100644 --- a/services/azure-devops/azure-devops-coverage.spec.js +++ b/services/azure-devops/azure-devops-coverage.spec.js @@ -4,7 +4,7 @@ import AzureDevOpsCoverage from './azure-devops-coverage.service.js' describe('AzureDevOpsCoverage', function () { describe('auth', function () { it('sends the auth information as configured', async function () { - testAuth( + return testAuth( AzureDevOpsCoverage, 'BasicAuth', { diff --git a/services/azure-devops/azure-devops-tests.spec.js b/services/azure-devops/azure-devops-tests.spec.js index 9bd757573802a..3a4858784c617 100644 --- a/services/azure-devops/azure-devops-tests.spec.js +++ b/services/azure-devops/azure-devops-tests.spec.js @@ -4,7 +4,7 @@ import AzureDevOpsTests from './azure-devops-tests.service.js' describe('AzureDevOpsTests', function () { describe('auth', function () { it('sends the auth information as configured', async function () { - testAuth( + return testAuth( AzureDevOpsTests, 'BasicAuth', { diff --git a/services/jira/jira-issue.spec.js b/services/jira/jira-issue.spec.js index b1e23d30ce87e..d70aa6aa1995d 100644 --- a/services/jira/jira-issue.spec.js +++ b/services/jira/jira-issue.spec.js @@ -5,7 +5,7 @@ import { config } from './jira-test-helpers.js' describe('JiraIssue', function () { describe('auth', function () { it('sends the auth information as configured', async function () { - testAuth( + return testAuth( JiraIssue, 'BasicAuth', { diff --git a/services/jira/jira-sprint.spec.js b/services/jira/jira-sprint.spec.js index e87fcafbcd32b..7ec69e9a6154a 100644 --- a/services/jira/jira-sprint.spec.js +++ b/services/jira/jira-sprint.spec.js @@ -5,7 +5,7 @@ import { config } from './jira-test-helpers.js' describe('JiraSprint', function () { describe('auth', function () { it('sends the auth information as configured', async function () { - testAuth( + return testAuth( JiraSprint, 'BasicAuth', { diff --git a/services/nexus/nexus.spec.js b/services/nexus/nexus.spec.js index 91f6ccbb6b4a6..e554a9300ea71 100644 --- a/services/nexus/nexus.spec.js +++ b/services/nexus/nexus.spec.js @@ -122,7 +122,7 @@ describe('Nexus', function () { }, } it('sends the auth information as configured', async function () { - testAuth( + return testAuth( Nexus, 'BasicAuth', { diff --git a/services/sonar/sonar-fortify-rating.spec.js b/services/sonar/sonar-fortify-rating.spec.js index 6538c473de996..b226d64762015 100644 --- a/services/sonar/sonar-fortify-rating.spec.js +++ b/services/sonar/sonar-fortify-rating.spec.js @@ -8,7 +8,7 @@ import { describe('SonarFortifyRating', function () { describe('auth', function () { it('sends the auth information as configured', async function () { - testAuth( + return testAuth( SonarFortifyRating, 'BasicAuth', legacySonarResponse('fortify-security-rating', 4), diff --git a/services/sonar/sonar-generic.spec.js b/services/sonar/sonar-generic.spec.js index 42c1910e0a071..acdaa38c2637a 100644 --- a/services/sonar/sonar-generic.spec.js +++ b/services/sonar/sonar-generic.spec.js @@ -8,18 +8,23 @@ import { describe('SonarGeneric', function () { describe('auth', function () { it('sends the auth information as configured', async function () { - testAuth(SonarGeneric, 'BasicAuth', legacySonarResponse('test', 903), { - configOverride: testAuthConfigOverride, - exampleOverride: { - component: 'test', - metricName: 'test', - branch: 'home', - server: - testAuthConfigOverride.public.services.sonar.authorizedOrigins[0], - sonarVersion: '4.2', + return testAuth( + SonarGeneric, + 'BasicAuth', + legacySonarResponse('test', 903), + { + configOverride: testAuthConfigOverride, + exampleOverride: { + component: 'test', + metricName: 'test', + branch: 'home', + server: + testAuthConfigOverride.public.services.sonar.authorizedOrigins[0], + sonarVersion: '4.2', + }, + ignoreOpenApiExample: true, }, - ignoreOpenApiExample: true, - }) + ) }) }) }) diff --git a/services/symfony/symfony-insight-grade.spec.js b/services/symfony/symfony-insight-grade.spec.js index 3df9fb7bb3393..9db2cbc6d2f92 100644 --- a/services/symfony/symfony-insight-grade.spec.js +++ b/services/symfony/symfony-insight-grade.spec.js @@ -4,7 +4,7 @@ import SymfonyInsightGrade from './symfony-insight-grade.service.js' describe('SymfonyInsightGrade', function () { describe('auth', function () { it('sends the auth information as configured', async function () { - testAuth( + return testAuth( SymfonyInsightGrade, 'BasicAuth', ` diff --git a/services/symfony/symfony-insight-stars.spec.js b/services/symfony/symfony-insight-stars.spec.js index 039d479581929..bf121d6c470a4 100644 --- a/services/symfony/symfony-insight-stars.spec.js +++ b/services/symfony/symfony-insight-stars.spec.js @@ -4,7 +4,7 @@ import SymfonyInsightStars from './symfony-insight-stars.service.js' describe('SymfonyInsightStars', function () { describe('auth', function () { it('sends the auth information as configured', async function () { - testAuth( + return testAuth( SymfonyInsightStars, 'BasicAuth', ` diff --git a/services/symfony/symfony-insight-violations.spec.js b/services/symfony/symfony-insight-violations.spec.js index bee2a5cd3c026..8fac86cf87e20 100644 --- a/services/symfony/symfony-insight-violations.spec.js +++ b/services/symfony/symfony-insight-violations.spec.js @@ -4,7 +4,7 @@ import SymfonyInsightViolations from './symfony-insight-violations.service.js' describe('SymfonyInsightViolations', function () { describe('auth', function () { it('sends the auth information as configured', async function () { - testAuth( + return testAuth( SymfonyInsightViolations, 'BasicAuth', ` diff --git a/services/teamcity/teamcity-build.spec.js b/services/teamcity/teamcity-build.spec.js index 2de605578fa6f..91ba4c6a85253 100644 --- a/services/teamcity/teamcity-build.spec.js +++ b/services/teamcity/teamcity-build.spec.js @@ -5,7 +5,7 @@ import { config } from './teamcity-test-helpers.js' describe('TeamCityBuild', function () { describe('auth', function () { it('sends the auth information as configured', async function () { - testAuth( + return testAuth( TeamCityBuild, 'BasicAuth', { status: 'SUCCESS', statusText: 'Success' }, diff --git a/services/teamcity/teamcity-coverage.spec.js b/services/teamcity/teamcity-coverage.spec.js index c1dcb8870551b..7bec5ef4c774e 100644 --- a/services/teamcity/teamcity-coverage.spec.js +++ b/services/teamcity/teamcity-coverage.spec.js @@ -5,7 +5,7 @@ import { config } from './teamcity-test-helpers.js' describe('TeamCityCoverage', function () { describe('auth', function () { it('sends the auth information as configured', async function () { - testAuth( + return testAuth( TeamCityCoverage, 'BasicAuth', { From 263aba7cb276fd0e6adfd7832c473cc95ffa966e Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sat, 8 Jun 2024 17:38:34 +0300 Subject: [PATCH 45/50] Remove ignoreOpenApiExample option in testAuth function The `ignoreOpenApiExample` option in the `testAuth` function is no longer needed and has been removed. This option was used to ignore OpenAPI examples for classes without OpenAPI, but it is no longer necessary as the function now handles this case correctly. BaseService defines by dafault empty object for openApi so if nothing is set we expect empty obj. Warn devs about missing openApi as openApi is standard for services now. --- services/sonar/sonar-generic.spec.js | 1 - services/test-helpers.js | 20 ++++++-------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/services/sonar/sonar-generic.spec.js b/services/sonar/sonar-generic.spec.js index acdaa38c2637a..d1cf759fef550 100644 --- a/services/sonar/sonar-generic.spec.js +++ b/services/sonar/sonar-generic.spec.js @@ -22,7 +22,6 @@ describe('SonarGeneric', function () { testAuthConfigOverride.public.services.sonar.authorizedOrigins[0], sonarVersion: '4.2', }, - ignoreOpenApiExample: true, }, ) }) diff --git a/services/test-helpers.js b/services/test-helpers.js index b4654ef491339..3d8b50da5e521 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -57,10 +57,11 @@ function getBadgeExampleCall(serviceClass, paramType) { ) } - if (!serviceClass.openApi) { - throw new TypeError( - `Missing OpenAPI in service class ${serviceClass.name}.`, + if (Object.keys(serviceClass.openApi).length === 0) { + console.warn( + `Missing OpenAPI in service class ${serviceClass.name}. Make sure to use exampleOverride in testAuth.`, ) + return {} } if (!['path', 'query'].includes(paramType)) { throw new TypeError('Invalid paramType: Must be path or query.') @@ -227,7 +228,6 @@ function fakeJwtToken() { * @param {object} options.exampleOverride - Override example params in test. * @param {object} options.authOverride - Override class auth params. * @param {object} options.configOverride - Override the config for this test. - * @param {boolean} options.ignoreOpenApiExample - For classes without OpenApi example ignore for usage of override examples only * @param {boolean} options.multipleRequests - For classes that require multiple requests to complete the test. * @throws {TypeError} - Throws a TypeError if the input `serviceClass` is not an instance of BaseService, * or if `serviceClass` is missing authorizedOrigins. @@ -253,7 +253,6 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { exampleOverride = {}, authOverride, configOverride, - ignoreOpenApiExample = false, multipleRequests = false, } = options if (contentType && typeof contentType !== 'string') { @@ -275,9 +274,6 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { if (configOverride && typeof configOverride !== 'object') { throw new TypeError('Invalid configOverride: Must be an Object.') } - if (ignoreOpenApiExample && typeof ignoreOpenApiExample !== 'boolean') { - throw new TypeError('Invalid ignoreOpenApiExample: Must be an Object.') - } if (multipleRequests && typeof multipleRequests !== 'boolean') { throw new TypeError('Invalid multipleRequests: Must be an Object.') } @@ -310,12 +306,8 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { authOrigins, authOverride, ) - const exampleInvokePathParams = ignoreOpenApiExample - ? undefined - : getBadgeExampleCall(serviceClass, 'path') - const exampleInvokeQueryParams = ignoreOpenApiExample - ? undefined - : getBadgeExampleCall(serviceClass, 'query') + const exampleInvokePathParams = getBadgeExampleCall(serviceClass, 'path') + const exampleInvokeQueryParams = getBadgeExampleCall(serviceClass, 'query') if (options && typeof options !== 'object') { throw new TypeError('Invalid options: Must be an object.') } From 669d9137c271d7a13d39cfaf36d8caa8f5a53bbb Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sun, 9 Jun 2024 00:06:52 +0300 Subject: [PATCH 46/50] handle extraction of openApi boolean examples Improve authTest by handeling boolean example extractions. Remove example override not needed after this change. --- services/azure-devops/azure-devops-tests.spec.js | 3 --- services/jenkins/jenkins-tests.spec.js | 1 - services/test-helpers.js | 6 +++++- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/services/azure-devops/azure-devops-tests.spec.js b/services/azure-devops/azure-devops-tests.spec.js index 3a4858784c617..c139576779584 100644 --- a/services/azure-devops/azure-devops-tests.spec.js +++ b/services/azure-devops/azure-devops-tests.spec.js @@ -24,9 +24,6 @@ describe('AzureDevOpsTests', function () { ], }, { - exampleOverride: { - compact_message: undefined, - }, multipleRequests: true, }, ) diff --git a/services/jenkins/jenkins-tests.spec.js b/services/jenkins/jenkins-tests.spec.js index f2404f2005013..14eaa5b90a602 100644 --- a/services/jenkins/jenkins-tests.spec.js +++ b/services/jenkins/jenkins-tests.spec.js @@ -20,7 +20,6 @@ describe('JenkinsTests', function () { { actions: [{ totalCount: 3, failCount: 2, skipCount: 1 }] }, { configOverride: authConfigOverride, - exampleOverride: { compact_message: '' }, }, ) }) diff --git a/services/test-helpers.js b/services/test-helpers.js index 3d8b50da5e521..a2079b02a812e 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -80,7 +80,11 @@ function getBadgeExampleCall(serviceClass, paramType) { // reformat structure for serviceClass.invoke const exampleInvokeParams = firstOpenapiExampleParams.reduce((acc, obj) => { if (obj.in === paramType) { - acc[obj.name] = obj.example + let example = obj.example + if (obj?.schema?.type === 'boolean') { + example = example || '' + } + acc[obj.name] = example } return acc }, {}) From 625042e6beb7c8285fb46afd9079d341f1150e3f Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Wed, 12 Jun 2024 14:45:00 +0300 Subject: [PATCH 47/50] Use static headers in base service classes and remove contentType option from testAuth Refactor the base service classes (BaseGraphqlService, BaseJsonService, BaseSvgScrapingService, BaseTomlService, BaseXmlService, BaseYamlService) to use static headers for access from other function to header types. Remove testAuth contentType option and use the new headers veriable to extract headers insted. --- core/base-service/base-graphql.js | 4 +++- core/base-service/base-json.js | 4 +++- core/base-service/base-svg-scraping.js | 4 +++- core/base-service/base-toml.js | 18 +++++++++--------- core/base-service/base-xml.js | 4 +++- core/base-service/base-yaml.js | 12 ++++++------ services/test-helpers.js | 9 +++------ 7 files changed, 30 insertions(+), 25 deletions(-) diff --git a/core/base-service/base-graphql.js b/core/base-service/base-graphql.js index 650e6fb549a82..e70a81c0d4c52 100644 --- a/core/base-service/base-graphql.js +++ b/core/base-service/base-graphql.js @@ -27,6 +27,8 @@ class BaseGraphqlService extends BaseService { return parseJson(buffer) } + static headers = { Accept: 'application/json' } + /** * Request data from an upstream GraphQL API, * parse it and validate against a schema @@ -76,7 +78,7 @@ class BaseGraphqlService extends BaseService { transformErrors = defaultTransformErrors, }) { const mergedOptions = { - ...{ headers: { Accept: 'application/json' } }, + ...{ headers: this.constructor.headers }, ...options, } mergedOptions.method = 'POST' diff --git a/core/base-service/base-json.js b/core/base-service/base-json.js index 281eebe6b4778..c87bafe038b2c 100644 --- a/core/base-service/base-json.js +++ b/core/base-service/base-json.js @@ -21,6 +21,8 @@ class BaseJsonService extends BaseService { return parseJson(buffer) } + static headers = { Accept: 'application/json' } + /** * Request data from an upstream API serving JSON, * parse it and validate against a schema @@ -54,7 +56,7 @@ class BaseJsonService extends BaseService { logErrors = [429], }) { const mergedOptions = { - ...{ headers: { Accept: 'application/json' } }, + ...{ headers: this.constructor.headers }, ...options, } const { buffer } = await this._request({ diff --git a/core/base-service/base-svg-scraping.js b/core/base-service/base-svg-scraping.js index 7eacea6f7dab6..8d992353cb0a4 100644 --- a/core/base-service/base-svg-scraping.js +++ b/core/base-service/base-svg-scraping.js @@ -42,6 +42,8 @@ class BaseSvgScrapingService extends BaseService { } } + static headers = { Accept: 'image/svg+xml' } + /** * Request data from an endpoint serving SVG, * parse a value from it and validate against a schema @@ -79,7 +81,7 @@ class BaseSvgScrapingService extends BaseService { }) { const logTrace = (...args) => trace.logTrace('fetch', ...args) const mergedOptions = { - ...{ headers: { Accept: 'image/svg+xml' } }, + ...{ headers: this.constructor.headers }, ...options, } const { buffer } = await this._request({ diff --git a/core/base-service/base-toml.js b/core/base-service/base-toml.js index 435f6bafbefea..883423b37abe8 100644 --- a/core/base-service/base-toml.js +++ b/core/base-service/base-toml.js @@ -14,6 +14,14 @@ import trace from './trace.js' * @abstract */ class BaseTomlService extends BaseService { + static headers = { + Accept: + // the official header should be application/toml - see https://toml.io/en/v1.0.0#mime-type + // but as this is not registered here https://www.iana.org/assignments/media-types/media-types.xhtml + // some apps use other mime-type like application/x-toml, text/plain etc.... + 'text/x-toml, text/toml, application/x-toml, application/toml, text/plain', + } + /** * Request data from an upstream API serving TOML, * parse it and validate against a schema @@ -48,15 +56,7 @@ class BaseTomlService extends BaseService { }) { const logTrace = (...args) => trace.logTrace('fetch', ...args) const mergedOptions = { - ...{ - headers: { - Accept: - // the official header should be application/toml - see https://toml.io/en/v1.0.0#mime-type - // but as this is not registered here https://www.iana.org/assignments/media-types/media-types.xhtml - // some apps use other mime-type like application/x-toml, text/plain etc.... - 'text/x-toml, text/toml, application/x-toml, application/toml, text/plain', - }, - }, + ...{ headers: this.constructor.headers }, ...options, } const { buffer } = await this._request({ diff --git a/core/base-service/base-xml.js b/core/base-service/base-xml.js index 4424abf608a7f..0afffeb1dc846 100644 --- a/core/base-service/base-xml.js +++ b/core/base-service/base-xml.js @@ -15,6 +15,8 @@ import { InvalidResponse } from './errors.js' * @abstract */ class BaseXmlService extends BaseService { + static headers = { Accept: 'application/xml, text/xml' } + /** * Request data from an upstream API serving XML, * parse it and validate against a schema @@ -53,7 +55,7 @@ class BaseXmlService extends BaseService { }) { const logTrace = (...args) => trace.logTrace('fetch', ...args) const mergedOptions = { - ...{ headers: { Accept: 'application/xml, text/xml' } }, + ...{ headers: this.constructor.headers }, ...options, } const { buffer } = await this._request({ diff --git a/core/base-service/base-yaml.js b/core/base-service/base-yaml.js index 9cb700a18442b..e0d19a42a270b 100644 --- a/core/base-service/base-yaml.js +++ b/core/base-service/base-yaml.js @@ -14,6 +14,11 @@ import trace from './trace.js' * @abstract */ class BaseYamlService extends BaseService { + static headers = { + Accept: + 'text/x-yaml, text/yaml, application/x-yaml, application/yaml, text/plain', + } + /** * Request data from an upstream API serving YAML, * parse it and validate against a schema @@ -50,12 +55,7 @@ class BaseYamlService extends BaseService { }) { const logTrace = (...args) => trace.logTrace('fetch', ...args) const mergedOptions = { - ...{ - headers: { - Accept: - 'text/x-yaml, text/yaml, application/x-yaml, application/yaml, text/plain', - }, - }, + ...{ headers: this.constructor.headers }, ...options, } const { buffer } = await this._request({ diff --git a/services/test-helpers.js b/services/test-helpers.js index a2079b02a812e..de76ac53829ca 100644 --- a/services/test-helpers.js +++ b/services/test-helpers.js @@ -223,7 +223,6 @@ function fakeJwtToken() { * @param {'BasicAuth'|'ApiKeyHeader'|'BearerAuthHeader'|'QueryStringAuth'|'JwtAuth'} authMethod The auth method of the tested service class. * @param {object} dummyResponse An object containing the dummy response by the server. * @param {object} options - Additional options for non default keys and content-type of the dummy response. - * @param {'application/xml'|'application/json'} options.contentType - Header for the response, may contain any string. * @param {string} options.apiHeaderKey - Non default header for ApiKeyHeader auth. * @param {string} options.bearerHeaderKey - Non default bearer header prefix for BearerAuthHeader. * @param {string} options.queryUserKey - QueryStringAuth user key. @@ -248,7 +247,6 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { } const { - contentType, apiHeaderKey = 'x-api-key', bearerHeaderKey = 'Bearer', queryUserKey, @@ -259,10 +257,9 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { configOverride, multipleRequests = false, } = options - if (contentType && typeof contentType !== 'string') { - throw new TypeError('Invalid contentType: Must be a String.') - } - const header = contentType ? { 'Content-Type': contentType } : undefined + const header = serviceClass.headers + ? { 'Content-Type': serviceClass.headers.Accept.split(', ')[0] } + : undefined if (!apiHeaderKey || typeof apiHeaderKey !== 'string') { throw new TypeError('Invalid apiHeaderKey: Must be a String.') } From 059b8ca7a2be5cc3e248bf6db8444f0760b21fff Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Wed, 12 Jun 2024 15:02:31 +0300 Subject: [PATCH 48/50] Remove contentType option tests using testAuth This option is removed --- services/symfony/symfony-insight-grade.spec.js | 1 - services/symfony/symfony-insight-stars.spec.js | 1 - services/symfony/symfony-insight-violations.spec.js | 1 - 3 files changed, 3 deletions(-) diff --git a/services/symfony/symfony-insight-grade.spec.js b/services/symfony/symfony-insight-grade.spec.js index 9db2cbc6d2f92..28bd368c062a6 100644 --- a/services/symfony/symfony-insight-grade.spec.js +++ b/services/symfony/symfony-insight-grade.spec.js @@ -11,7 +11,6 @@ describe('SymfonyInsightGrade', function () { gold finished `, - { contentType: 'application/vnd.com.sensiolabs.insight+xml' }, ) }) }) diff --git a/services/symfony/symfony-insight-stars.spec.js b/services/symfony/symfony-insight-stars.spec.js index bf121d6c470a4..e1a3deffb7502 100644 --- a/services/symfony/symfony-insight-stars.spec.js +++ b/services/symfony/symfony-insight-stars.spec.js @@ -11,7 +11,6 @@ describe('SymfonyInsightStars', function () { gold finished `, - { contentType: 'application/vnd.com.sensiolabs.insight+xml' }, ) }) }) diff --git a/services/symfony/symfony-insight-violations.spec.js b/services/symfony/symfony-insight-violations.spec.js index 8fac86cf87e20..0c20a3838a785 100644 --- a/services/symfony/symfony-insight-violations.spec.js +++ b/services/symfony/symfony-insight-violations.spec.js @@ -10,7 +10,6 @@ describe('SymfonyInsightViolations', function () { ` finished `, - { contentType: 'application/vnd.com.sensiolabs.insight+xml' }, ) }) }) From 73b3e14ac362e59ffedcd1747c1e650c3b6f4aea Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Thu, 3 Oct 2024 15:19:47 +0300 Subject: [PATCH 49/50] Update jenkinsCoverage response Example changed at 62ed7c3a277204aea2943adec2f63f3786d80def The new example schema differ from old one This caused test to fail Update example response to fit new schema --- services/jenkins/jenkins-coverage.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/jenkins/jenkins-coverage.spec.js b/services/jenkins/jenkins-coverage.spec.js index 653ed202fc477..eb8a11ac57931 100644 --- a/services/jenkins/jenkins-coverage.spec.js +++ b/services/jenkins/jenkins-coverage.spec.js @@ -5,7 +5,7 @@ const authConfigOverride = { public: { services: { jenkins: { - authorizedOrigins: ['https://jenkins.sqlalchemy.org'], + authorizedOrigins: ['https://ci-maven.apache.org'], }, }, }, @@ -17,7 +17,7 @@ describe('JenkinsCoverage', function () { return testAuth( JenkinsCoverage, 'BasicAuth', - { results: { elements: [{ name: 'Lines', ratio: 88 }] } }, + { instructionCoverage: { percentage: 93 } }, { configOverride: authConfigOverride }, ) }) From 1fdf395fcc227e794f806d2377120b39553272f9 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Thu, 3 Oct 2024 16:30:43 +0300 Subject: [PATCH 50/50] remove BitbucketPullReuqest tests remove for adding these changes in a later PR. this test adds the authOverride we try to get rid of. This blocks testAuth development and is planned for a later time. --- .../bitbucket-pull-request.service.js | 34 ++--- .../bitbucket/bitbucket-pull-request.spec.js | 134 ++++++++---------- 2 files changed, 73 insertions(+), 95 deletions(-) diff --git a/services/bitbucket/bitbucket-pull-request.service.js b/services/bitbucket/bitbucket-pull-request.service.js index 8eb142f188612..0e89e1d79c211 100644 --- a/services/bitbucket/bitbucket-pull-request.service.js +++ b/services/bitbucket/bitbucket-pull-request.service.js @@ -32,20 +32,6 @@ function pullRequestClassGenerator(raw) { queryParamSchema, } - static auth = { - userKey: 'bitbucket_username', - passKey: 'bitbucket_password', - serviceKey: 'bitbucket', - isRequired: true, - } - - static authServer = { - userKey: 'bitbucket_server_username', - passKey: 'bitbucket_server_password', - serviceKey: 'bitbucketServer', - isRequired: true, - } - static get openApi() { const key = `/bitbucket/${routePrefix}/{user}/{repo}` const route = {} @@ -85,16 +71,27 @@ function pullRequestClassGenerator(raw) { constructor(context, config) { super(context, config) - // can only be set here as we must get config + this.bitbucketAuthHelper = new AuthHelper( + { + userKey: 'bitbucket_username', + passKey: 'bitbucket_password', + authorizedOrigins: ['https://bitbucket.org'], + }, + config, + ) this.bitbucketServerAuthHelper = new AuthHelper( - BitbucketPullRequest.authServer, + { + userKey: 'bitbucket_server_username', + passKey: 'bitbucket_server_password', + serviceKey: 'bitbucketServer', + }, config, ) } async fetchCloud({ user, repo }) { return this._requestJson( - this.authHelper.withBasicAuth({ + this.bitbucketAuthHelper.withBasicAuth({ url: `https://bitbucket.org/api/2.0/repositories/${user}/${repo}/pullrequests/`, schema, options: { searchParams: { state: 'OPEN', limit: 0 } }, @@ -106,7 +103,7 @@ function pullRequestClassGenerator(raw) { // https://docs.atlassian.com/bitbucket-server/rest/5.16.0/bitbucket-rest.html#idm46229602363312 async fetchServer({ server, user, repo }) { return this._requestJson( - this.authHelper.withBasicAuth({ + this.bitbucketServerAuthHelper.withBasicAuth({ url: `${server}/rest/api/1.0/projects/${user}/repos/${repo}/pull-requests`, schema, options: { @@ -124,7 +121,6 @@ function pullRequestClassGenerator(raw) { async fetch({ server, user, repo }) { if (server !== undefined) { - this.authHelper = this.bitbucketServerAuthHelper return this.fetchServer({ server, user, repo }) } else { return this.fetchCloud({ user, repo }) diff --git a/services/bitbucket/bitbucket-pull-request.spec.js b/services/bitbucket/bitbucket-pull-request.spec.js index 5a6622f264379..ff504d1cd428b 100644 --- a/services/bitbucket/bitbucket-pull-request.spec.js +++ b/services/bitbucket/bitbucket-pull-request.spec.js @@ -1,91 +1,73 @@ -import { testAuth } from '../test-helpers.js' -import { - BitbucketRawPullRequests, - BitbucketNonRawPullRequests, -} from './bitbucket-pull-request.service.js' +import { expect } from 'chai' +import nock from 'nock' +import { cleanUpNockAfterEach, defaultContext } from '../test-helpers.js' +import { BitbucketRawPullRequests } from './bitbucket-pull-request.service.js' -const serverConfigOverride = { - public: { - services: { - bitbucketServer: { - authorizedOrigins: ['https://bitbucket.mydomain.net'], - }, - bitbucket: { - authorizedOrigins: ['https://bitbucket.org'], - }, - }, - }, - private: { - bitbucket_username: 'must-be-set-for-class-constructor', - bitbucket_password: 'must-be-set-for-class-constructor', - }, -} +describe('BitbucketPullRequest', function () { + cleanUpNockAfterEach() -const cloudConfigOverride = { - public: { - services: { - bitbucket: { - authorizedOrigins: ['https://bitbucket.org'], - }, - bitbucketServer: { - authorizedOrigins: [], - }, - }, - }, -} + const user = 'admin' + const pass = 'password' -describe('BitbucketRawPullRequests', function () { - describe('auth', function () { - it('sends the auth information to Bitbucket cloud as configured', async function () { - return testAuth( - BitbucketRawPullRequests, - 'BasicAuth', - { size: 42 }, - { - exampleOverride: { server: undefined }, - configOverride: cloudConfigOverride, - }, - ) - }) + it('Sends auth headers to Bitbucket as configured', async function () { + const scope = nock('https://bitbucket.org/api/2.0/repositories/') + .get(/.*/) + .basicAuth({ user, pass }) + .reply(200, { size: 42 }) - it('sends the auth information to Bitbucket instence as configured', async function () { - return testAuth( - BitbucketRawPullRequests, - 'BasicAuth', - { size: 42 }, + expect( + await BitbucketRawPullRequests.invoke( + defaultContext, { - authOverride: BitbucketRawPullRequests.authServer, - configOverride: serverConfigOverride, + public: { + services: { + bitbucketServer: { + authorizedOrigins: [], + }, + }, + }, + private: { bitbucket_username: user, bitbucket_password: pass }, }, - ) + { user: 'shields-io', repo: 'test-repo' }, + ), + ).to.deep.equal({ + message: '42', + color: 'yellow', }) + + scope.done() }) -}) -describe('BitbucketNonRawPullRequests', function () { - describe('auth', function () { - it('sends the auth information to Bitbucket cloud as configured', async function () { - return testAuth( - BitbucketNonRawPullRequests, - 'BasicAuth', - { size: 42 }, - { - exampleOverride: { server: undefined }, - configOverride: cloudConfigOverride, - }, - ) - }) + it('Sends auth headers to Bitbucket Server as configured', async function () { + const scope = nock('https://bitbucket.example.test/rest/api/1.0/projects') + .get(/.*/) + .basicAuth({ user, pass }) + .reply(200, { size: 42 }) - it('sends the auth information to Bitbucket instence as configured', async function () { - return testAuth( - BitbucketNonRawPullRequests, - 'BasicAuth', - { size: 42 }, + expect( + await BitbucketRawPullRequests.invoke( + defaultContext, { - authOverride: BitbucketNonRawPullRequests.authServer, - configOverride: serverConfigOverride, + public: { + services: { + bitbucketServer: { + authorizedOrigins: ['https://bitbucket.example.test'], + }, + }, + }, + private: { + bitbucket_server_username: user, + bitbucket_server_password: pass, + }, }, - ) + { user: 'project', repo: 'repo' }, + { server: 'https://bitbucket.example.test' }, + ), + ).to.deep.equal({ + message: '42', + color: 'yellow', }) + + scope.done() }) })