From f48e129c0dd070037b34e50b0a466dd1dba3f538 Mon Sep 17 00:00:00 2001 From: Erika Wallace Date: Mon, 4 May 2026 14:56:07 -0400 Subject: [PATCH 1/2] update analytics tests to use the credential manager --- cspell-dictionary.txt | 1 + test/unit/analytics.unit.test.ts | 258 +++++++++++++------------------ 2 files changed, 112 insertions(+), 147 deletions(-) diff --git a/cspell-dictionary.txt b/cspell-dictionary.txt index 1317ab8354..55d83f59bb 100644 --- a/cspell-dictionary.txt +++ b/cspell-dictionary.txt @@ -29,6 +29,7 @@ autoloaded autoscale autovacuum awscli +badheaders barsize baseport bindkey diff --git a/test/unit/analytics.unit.test.ts b/test/unit/analytics.unit.test.ts index b4d61dd7ca..2ead06147c 100644 --- a/test/unit/analytics.unit.test.ts +++ b/test/unit/analytics.unit.test.ts @@ -1,11 +1,11 @@ import {Config} from '@oclif/core' import {expect} from 'chai' import nock from 'nock' -import netrc from 'netrc-parser' -import {vars} from '@heroku-cli/command' +import * as sinon from 'sinon' import AnalyticsCommand, {AnalyticsInterface} from '../../src/lib/analytics-telemetry/backboard-herokulytics-client.js' import HerokulyticsConfig from '../../src/lib/analytics-telemetry/herokulytics-config.js' +import {stubCredentialManager} from '../helpers/credential-manager-stub.js' const mockCommand = { plugin: { @@ -53,8 +53,7 @@ async function runAnalyticsTest(expectedCbk: (data: AnalyticsInterface) => any, backboard.done() } -/* -describe('analytics (backboard has an error) with authorizationToken', function () { +describe('analytics (error handling)', function () { let sandbox: any before(async function () { @@ -62,8 +61,18 @@ describe('analytics (backboard has an error) with authorizationToken', function sandbox.stub(HerokulyticsConfig.prototype, 'install').get(() => 'abcde') }) - it('does not show an error on console', async function () { - const backboard = nock('https://backboard.heroku.com/') + after(function () { + sandbox.restore() + }) + + it('does not show an error on console when backboard has an error (with authorizationToken)', async function () { + const credentialManagerStub = stubCredentialManager({ + getAuth: () => Promise.resolve({account: 'test@example.com', token: 'test-token'}), + }) + + const backboard = nock('https://backboard.heroku.com/', { + reqheaders: {authorization: 'Bearer test-token'}, + }) .get('/hamurai') .query(() => true) .reply(500) @@ -81,13 +90,25 @@ describe('analytics (backboard has an error) with authorizationToken', function Command: mockCommand as any, argv: ['foo', 'bar'], }) } catch { - throw new Error('Expected analytics hook to 🦃 error') + throw new Error('Expected analytics.send to catch error') } finally { backboard.done() + credentialManagerStub.restore() } }) - it('does not record if plugin is not present', async function () { + it('does not show an error on console when backboard has an error (without authorizationToken)', async function () { + const credentialManagerStub = stubCredentialManager({ + getAuth: () => Promise.resolve({account: undefined, token: undefined}), + }) + + const backboard = nock('https://backboard.heroku.com/', { + badheaders: ['authorization'], + }) + .get('/hamurai') + .query(() => true) + .reply(500) + const config = await Config.load() config.platform = 'win32' config.shell = 'fish' @@ -98,170 +119,113 @@ describe('analytics (backboard has an error) with authorizationToken', function try { await analytics.send({ - Command: mockInvalidCommand as any, argv: ['foo', 'bar'], + Command: mockCommand as any, argv: ['foo', 'bar'], }) } catch { - throw new Error('Expected analytics hook to 🦃 error') + throw new Error('Expected analytics.send to catch error') + } finally { + backboard.done() + credentialManagerStub.restore() } }) - describe('analytics (backboard has an error) without authorizationToken', function () { - let sandbox: any - let analyticsSandbox: any - - before(async function () { - sandbox = sinon.createSandbox() - sandbox.stub(HerokulyticsConfig.prototype, 'install').get(() => 'abcde') - analyticsSandbox = sinon.createSandbox() - analyticsSandbox.stub(AnalyticsCommand.prototype, 'netrcToken').get(() => '') - }) + it('does not record if plugin is not present', async function () { + const config = await Config.load() + config.platform = 'win32' + config.shell = 'fish' + config.version = '1' + config.userAgent = '@oclif/command/1.5.6 darwin-x64 node-v10.2.1' + config.name = 'heroku' + const analytics = new AnalyticsCommand(config) - it('does not show an error on console', async function () { - const backboard = nock('https://backboard.heroku.com/') - .get('/hamurai') - .query(() => true) - .reply(500) - - const config = await Config.load() - config.platform = 'win32' - config.shell = 'fish' - config.version = '1' - config.userAgent = '@oclif/command/1.5.6 darwin-x64 node-v10.2.1' - config.name = 'heroku' - const analytics = new AnalyticsCommand(config) - - try { - await analytics.send({ - Command: mockCommand as any, argv: ['foo', 'bar'], - }) - } catch { - throw new Error('Expected analytics hook to 🦃 error') - } finally { - backboard.done() - } - }) + try { + await analytics.send({ + Command: mockInvalidCommand as any, argv: ['foo', 'bar'], + }) + } catch { + throw new Error('Expected analytics.send to catch error') + } }) +}) - describe('analytics', function () { - let sandbox: any - - before(async function () { - sandbox = sinon.createSandbox() - sandbox.stub(HerokulyticsConfig.prototype, 'install').get(() => 'abcde') - }) - - it('emits source', async function () { - await runAnalyticsTest((d: AnalyticsInterface) => d.source, 'cli') - }) +describe('analytics', function () { + let sandbox: any - it('emits event', async function () { - await runAnalyticsTest((d: AnalyticsInterface) => d.event, 'login') - }) + before(async function () { + sandbox = sinon.createSandbox() + sandbox.stub(HerokulyticsConfig.prototype, 'install').get(() => 'abcde') + }) - it('emits property cli', async function () { - await runAnalyticsTest((d: AnalyticsInterface) => d.properties.cli, 'heroku') - }) + after(function () { + sandbox.restore() + }) - it('emits property command', async function () { - await runAnalyticsTest((d: AnalyticsInterface) => d.properties.command, 'login') - }) + it('emits source', async function () { + await runAnalyticsTest((d: AnalyticsInterface) => d.source, 'cli') + }) - it('emits property completion', async function () { - await runAnalyticsTest((d: AnalyticsInterface) => d.properties.completion, 0) - }) + it('emits event', async function () { + await runAnalyticsTest((d: AnalyticsInterface) => d.event, 'login') + }) - it('emits property version', async function () { - await runAnalyticsTest((d: AnalyticsInterface) => d.properties.version, '1') - }) + it('emits property cli', async function () { + await runAnalyticsTest((d: AnalyticsInterface) => d.properties.cli, 'heroku') + }) - it('emits property plugin', async function () { - await runAnalyticsTest((d: AnalyticsInterface) => d.properties.plugin, 'foo') - }) + it('emits property command', async function () { + await runAnalyticsTest((d: AnalyticsInterface) => d.properties.command, 'login') + }) - it('emits property plugin_version', async function () { - await runAnalyticsTest((d: AnalyticsInterface) => d.properties.plugin_version, '123') - }) + it('emits property completion', async function () { + await runAnalyticsTest((d: AnalyticsInterface) => d.properties.completion, 0) + }) - it('emits property os', async function () { - await runAnalyticsTest((d: AnalyticsInterface) => d.properties.os, 'win32') - }) + it('emits property version', async function () { + await runAnalyticsTest((d: AnalyticsInterface) => d.properties.version, '1') + }) - it('emits property shell', async function () { - await runAnalyticsTest((d: AnalyticsInterface) => d.properties.shell, 'fish') - }) + it('emits property plugin', async function () { + await runAnalyticsTest((d: AnalyticsInterface) => d.properties.plugin, 'foo') + }) - it('emits property valid', async function () { - await runAnalyticsTest((d: AnalyticsInterface) => d.properties.valid, true) - }) + it('emits property plugin_version', async function () { + await runAnalyticsTest((d: AnalyticsInterface) => d.properties.plugin_version, '123') + }) - it('emits property language', async function () { - await runAnalyticsTest((d: AnalyticsInterface) => d.properties.language, 'node') - }) + it('emits property os', async function () { + await runAnalyticsTest((d: AnalyticsInterface) => d.properties.os, 'win32') + }) - it('emits property install_id', async function () { - await runAnalyticsTest((d: AnalyticsInterface) => d.properties.install_id, 'abcde') - }) + it('emits property shell', async function () { + await runAnalyticsTest((d: AnalyticsInterface) => d.properties.shell, 'fish') + }) - it('includes MCP version in version string when in MCP mode', async function () { - // Save original env vars - const originalMcpMode = process.env.HEROKU_MCP_MODE - const originalMcpVersion = process.env.HEROKU_MCP_SERVER_VERSION - - // Set MCP mode and version - process.env.HEROKU_MCP_MODE = 'true' - process.env.HEROKU_MCP_SERVER_VERSION = '1.2.3' - - try { - await runAnalyticsTest( - (d: AnalyticsInterface) => d.properties.version, - '1 (MCP 1.2.3)', // '1' is the version set in runAnalyticsTest - ) - } finally { - // Restore original env vars - process.env.HEROKU_MCP_MODE = originalMcpMode - process.env.HEROKU_MCP_SERVER_VERSION = originalMcpVersion - } - }) + it('emits property valid', async function () { + await runAnalyticsTest((d: AnalyticsInterface) => d.properties.valid, true) + }) - after(function () { - sandbox.restore() - }) + it('emits property language', async function () { + await runAnalyticsTest((d: AnalyticsInterface) => d.properties.language, 'node') }) - describe('analytics additional methods', function () { - let user: any + it('emits property install_id', async function () { + await runAnalyticsTest((d: AnalyticsInterface) => d.properties.install_id, 'abcde') + }) - beforeEach(function () { - process.env.HEROKU_API_KEY = 'testHerokuAPIKey' - user = netrc.machines[vars.apiHost]?.login || undefined - }) + it('includes MCP version in version string when in MCP mode', async function () { + const originalMcpMode = process.env.HEROKU_MCP_MODE + const originalMcpVersion = process.env.HEROKU_MCP_SERVER_VERSION + process.env.HEROKU_MCP_MODE = 'true' + process.env.HEROKU_MCP_SERVER_VERSION = '1.2.3' - it('retrieves user heroku API key', async function () { - const config = await Config.load() - config.platform = 'win32' - config.shell = 'fish' - config.version = '1' - config.userAgent = '@oclif/command/1.5.6 darwin-x64 node-v10.2.1' - config.name = 'heroku' - const analytics = new AnalyticsCommand(config) - const usingHerokuAPIKeyResult = analytics.usingHerokuAPIKey - const netrcLoginResult = analytics.netrcLogin - let userResult = analytics.user - - expect(usingHerokuAPIKeyResult).to.equal(true) - expect(netrcLoginResult).to.equal(user) - - // The result will be undefined since - // the method being accessed returns - // if the heroku API env var is present - expect(userResult).to.equal(undefined) - - // Remove heroku API env var - delete process.env.HEROKU_API_KEY - userResult = analytics.user - expect(userResult).to.equal(user) - }) + try { + await runAnalyticsTest( + (d: AnalyticsInterface) => d.properties.version, + '1 (MCP 1.2.3)', // '1' is the version set in runAnalyticsTest + ) + } finally { + process.env.HEROKU_MCP_MODE = originalMcpMode + process.env.HEROKU_MCP_SERVER_VERSION = originalMcpVersion + } }) }) - -*/ From 0b4ffa6bcb6e844748fab671fc82603baabd3746 Mon Sep 17 00:00:00 2001 From: Erika Wallace Date: Mon, 4 May 2026 15:12:37 -0400 Subject: [PATCH 2/2] rename and move analytics.unit.test.ts --- .../backboard-herokulytics-client.unit.test.ts} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename test/unit/{analytics.unit.test.ts => analytics-telemetry/backboard-herokulytics-client.unit.test.ts} (95%) diff --git a/test/unit/analytics.unit.test.ts b/test/unit/analytics-telemetry/backboard-herokulytics-client.unit.test.ts similarity index 95% rename from test/unit/analytics.unit.test.ts rename to test/unit/analytics-telemetry/backboard-herokulytics-client.unit.test.ts index 2ead06147c..a900ff8ce7 100644 --- a/test/unit/analytics.unit.test.ts +++ b/test/unit/analytics-telemetry/backboard-herokulytics-client.unit.test.ts @@ -3,9 +3,9 @@ import {expect} from 'chai' import nock from 'nock' import * as sinon from 'sinon' -import AnalyticsCommand, {AnalyticsInterface} from '../../src/lib/analytics-telemetry/backboard-herokulytics-client.js' -import HerokulyticsConfig from '../../src/lib/analytics-telemetry/herokulytics-config.js' -import {stubCredentialManager} from '../helpers/credential-manager-stub.js' +import AnalyticsCommand, {AnalyticsInterface} from '../../../src/lib/analytics-telemetry/backboard-herokulytics-client.js' +import HerokulyticsConfig from '../../../src/lib/analytics-telemetry/herokulytics-config.js' +import {stubCredentialManager} from '../../helpers/credential-manager-stub.js' const mockCommand = { plugin: {