diff --git a/config/custom-environment-variables.yml b/config/custom-environment-variables.yml index bbe982eadff33..21b529e373726 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 cea2edad7b60d..1edf706af3fea 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/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/core/server/server.js b/core/server/server.js index e50ae7392b8d9..21c0c83a99a99 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/azure-devops/azure-devops-coverage.spec.js b/services/azure-devops/azure-devops-coverage.spec.js new file mode 100644 index 0000000000000..9c05337bd0db0 --- /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 () { + return 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..c139576779584 --- /dev/null +++ b/services/azure-devops/azure-devops-tests.spec.js @@ -0,0 +1,32 @@ +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 () { + return testAuth( + AzureDevOpsTests, + 'BasicAuth', + { + aggregatedResultsAnalysis: { + totalTests: 95, + resultsByOutcome: { + Passed: { + count: 93, + }, + }, + }, + count: 1, + value: [ + { + id: 90395, + }, + ], + }, + { + multipleRequests: true, + }, + ) + }) + }) +}) 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..eb8a11ac57931 --- /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://ci-maven.apache.org'], + }, + }, + }, +} + +describe('JenkinsCoverage', function () { + describe('auth', function () { + it('sends the auth information as configured', async function () { + return testAuth( + JenkinsCoverage, + 'BasicAuth', + { instructionCoverage: { percentage: 93 } }, + { configOverride: authConfigOverride }, + ) + }) + }) +}) diff --git a/services/jenkins/jenkins-tests.spec.js b/services/jenkins/jenkins-tests.spec.js new file mode 100644 index 0000000000000..14eaa5b90a602 --- /dev/null +++ b/services/jenkins/jenkins-tests.spec.js @@ -0,0 +1,27 @@ +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, + }, + ) + }) + }) +}) diff --git a/services/jira/jira-issue.spec.js b/services/jira/jira-issue.spec.js index 4d3cbfdd3911f..d70aa6aa1995d 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 () { + return 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..7ec69e9a6154a 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 () { + return 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 } diff --git a/services/nexus/nexus.spec.js b/services/nexus/nexus.spec.js index 1f4114bfdc61a..e554a9300ea71 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', + return 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 }, + ) }) }) }) 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..b226d64762015 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 () { + return 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..d1cf759fef550 --- /dev/null +++ b/services/sonar/sonar-generic.spec.js @@ -0,0 +1,29 @@ +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 () { + 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', + }, + }, + ) + }) + }) +}) 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' }, + }, + ) + }) + }) }) diff --git a/services/symfony/symfony-insight-grade.spec.js b/services/symfony/symfony-insight-grade.spec.js new file mode 100644 index 0000000000000..28bd368c062a6 --- /dev/null +++ b/services/symfony/symfony-insight-grade.spec.js @@ -0,0 +1,17 @@ +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 () { + return testAuth( + SymfonyInsightGrade, + 'BasicAuth', + ` + gold + finished + `, + ) + }) + }) +}) diff --git a/services/symfony/symfony-insight-stars.spec.js b/services/symfony/symfony-insight-stars.spec.js new file mode 100644 index 0000000000000..e1a3deffb7502 --- /dev/null +++ b/services/symfony/symfony-insight-stars.spec.js @@ -0,0 +1,17 @@ +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 () { + return testAuth( + SymfonyInsightStars, + 'BasicAuth', + ` + gold + finished + `, + ) + }) + }) +}) diff --git a/services/symfony/symfony-insight-violations.spec.js b/services/symfony/symfony-insight-violations.spec.js new file mode 100644 index 0000000000000..0c20a3838a785 --- /dev/null +++ b/services/symfony/symfony-insight-violations.spec.js @@ -0,0 +1,16 @@ +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 () { + return testAuth( + SymfonyInsightViolations, + 'BasicAuth', + ` + finished + `, + ) + }) + }) +}) diff --git a/services/teamcity/teamcity-build.spec.js b/services/teamcity/teamcity-build.spec.js index 1e207bce56370..91ba4c6a85253 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 () { + return 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..7bec5ef4c774e 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 () { + return 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 } diff --git a/services/test-helpers.js b/services/test-helpers.js index b1368389c33c5..de76ac53829ca 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' @@ -37,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. @@ -48,17 +50,21 @@ 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.', ) } - 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.') } const firstOpenapiPath = Object.keys(serviceClass.openApi)[0] @@ -73,7 +79,13 @@ function getBadgeExampleCall(serviceClass) { // reformat structure for serviceClass.invoke const exampleInvokeParams = firstOpenapiExampleParams.reduce((acc, obj) => { - acc[obj.name] = obj.example + if (obj.in === paramType) { + let example = obj.example + if (obj?.schema?.type === 'boolean') { + example = example || '' + } + acc[obj.name] = example + } return acc }, {}) @@ -88,6 +100,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 +109,7 @@ function generateFakeConfig( fakeKey, fakeUser, fakeauthorizedOrigins, + authOverride, ) { if ( !serviceClass || @@ -106,37 +120,38 @@ 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.') } - 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 (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 = 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 + 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 { public: { services: { - [serviceClass.auth.serviceKey]: { + [auth.serviceKey]: { authorizedOrigins: fakeauthorizedOrigins, }, }, @@ -152,6 +167,8 @@ 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. + * @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. * @@ -160,7 +177,7 @@ function generateFakeConfig( * getServiceClassAuthOrigin(Obs) * // outputs 'https://api.opensuse.org' */ -function getServiceClassAuthOrigin(serviceClass) { +function getServiceClassAuthOrigin(serviceClass, authOverride, configOverride) { if ( !serviceClass || !serviceClass.prototype || @@ -170,12 +187,17 @@ 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, - ] + 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] } } @@ -201,12 +223,15 @@ 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. * @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. + * @param {object} options.configOverride - Override the config for this test. + * @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. * @@ -221,39 +246,72 @@ 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', bearerHeaderKey = 'Bearer', queryUserKey, queryPassKey, jwtLoginEndpoint, + exampleOverride = {}, + authOverride, + 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.') } if (!bearerHeaderKey || typeof bearerHeaderKey !== 'string') { throw new TypeError('Invalid bearerHeaderKey: Must be a String.') } + 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.') + } + if (configOverride && typeof configOverride !== 'object') { + throw new TypeError('Invalid configOverride: 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 + ? 'fake-user' + : auth.defaultToEmptyStringForUser + ? '' + : undefined + 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, + configOverride, + ) + const config = generateFakeConfig( + serviceClass, + fakeSecret, + fakeUser, + authOrigins, + authOverride, + ) + const exampleInvokePathParams = getBadgeExampleCall(serviceClass, 'path') + const exampleInvokeQueryParams = getBadgeExampleCall(serviceClass, 'query') + if (options && typeof options !== 'object') { + throw new TypeError('Invalid options: Must be an object.') + } if (!authOrigins) { throw new TypeError(`Missing authorizedOrigins for ${serviceClass.name}.`) @@ -263,6 +321,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': @@ -328,9 +389,29 @@ async function testAuth(serviceClass, authMethod, dummyResponse, options = {}) { }) expect( - await serviceClass.invoke(defaultContext, config, exampleInvokeParams), + await serviceClass.invoke( + defaultContext, + _.merge(config, configOverride), + { + ...exampleInvokePathParams, + ...exampleOverride, + }, + { + ...exampleInvokeQueryParams, + ...exampleOverride, + }, + ), ).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()) }