diff --git a/src/commands/addons/index.ts b/src/commands/addons/index.ts index b9a9a04853..3ea208114c 100644 --- a/src/commands/addons/index.ts +++ b/src/commands/addons/index.ts @@ -14,6 +14,13 @@ const ADDON_EXPANSION_HEADERS = { 'Accept-Expansion': 'addon_service,plan', } +// SDK resource types use required fields while @heroku-cli/schema uses +// optional fields for the same shapes. This cast bridges the gap until +// the codebase fully migrates off @heroku-cli/schema. +function asSchemaBased(value: unknown): T { + return value as T +} + const topic = 'addons' export default class Addons extends Command { @@ -72,22 +79,22 @@ async function addonGetter(api: APIClient, app?: string) { let attachmentsResponse: null | Promise = null let addonsResponse: Promise if (app) { // don't display attachments globally - addonsResponse = platformWithExpansion.addOn.listByApp(app) as unknown as Promise + addonsResponse = platformWithExpansion.addOn.listByApp(app).then(asSchemaBased) const sudoHeaders = JSON.parse(process.env.HEROKU_HEADERS || '{}') // eslint-disable-next-line unicorn/prefer-ternary if (sudoHeaders['X-Heroku-Sudo'] && !sudoHeaders['X-Heroku-Sudo-User']) { // because the root /addon-attachments endpoint won't include relevant // attachments when sudo-ing for another app, we will use the more // specific API call and sacrifice listing foreign attachments. - attachmentsResponse = platform.addOnAttachment.listByApp(app) as unknown as Promise + attachmentsResponse = platform.addOnAttachment.listByApp(app).then(asSchemaBased) } else { // In order to display all foreign attachments, we'll get out entire // attachment list - attachmentsResponse = platform.addOnAttachment.list() as unknown as Promise + attachmentsResponse = platform.addOnAttachment.list().then(asSchemaBased) } } else { // The global /addons endpoint doesn't support Accept-Expansion. - addonsResponse = platform.addOn.list() as unknown as Promise + addonsResponse = platform.addOn.list().then(asSchemaBased) } // Get addons and attachments in parallel diff --git a/src/commands/pg/backups/index.ts b/src/commands/pg/backups/index.ts index 4b886549f1..4dcb1631ff 100644 --- a/src/commands/pg/backups/index.ts +++ b/src/commands/pg/backups/index.ts @@ -1,12 +1,11 @@ import {Command, flags} from '@heroku-cli/command' -import {color, hux} from '@heroku/heroku-cli-util' +import {color, hux, utils} from '@heroku/heroku-cli-util' import {HerokuSDK} from '@heroku/sdk' +import {transferExtensions} from '@heroku/sdk/extensions/data' import {ux} from '@oclif/core/ux' -import type {BackupTransfer} from '../../../lib/pg/types.js' - import backupsFactory from '../../../lib/pg/backups.js' -import {listTransfersByApp} from '../../../lib/pg/sdk-adapter.js' +import type {BackupTransfer} from '../../../lib/pg/types.js' export default class Index extends Command { static description = 'list database backups' @@ -26,8 +25,8 @@ export default class Index extends Command { public async run(): Promise { const {flags: {app}} = await this.parse(Index) - const {data} = new HerokuSDK() - const transfers = await listTransfersByApp(data, app) + const {data} = new HerokuSDK({extensions: [transferExtensions]}) + const transfers = await data.transfer.listByApp(app) as BackupTransfer[] // NOTE that the sort order is descending transfers.sort((transferA, transferB) => transferB.created_at.localeCompare(transferA.created_at)) diff --git a/src/commands/pg/credentials.ts b/src/commands/pg/credentials.ts index dcea38c187..bb97dead74 100644 --- a/src/commands/pg/credentials.ts +++ b/src/commands/pg/credentials.ts @@ -2,11 +2,10 @@ import {Command, flags} from '@heroku-cli/command' import * as Heroku from '@heroku-cli/schema' import {hux, utils} from '@heroku/heroku-cli-util' import {HerokuSDK} from '@heroku/sdk' +import {postgresDatabaseExtensions} from '@heroku/sdk/extensions/data' +import type {CredentialInfo} from '@heroku/sdk/resources/data/postgres-database' import {Args} from '@oclif/core' -import type {NonAdvancedCredentialInfo} from '../../lib/data/types.js' - -import {listCredentials} from '../../lib/pg/sdk-adapter.js' import {presentCredentialAttachments} from '../../lib/pg/util.js' import {huxTableNoWrapOptions} from '../../lib/utils/table-utils.js' import {nls} from '../../nls.js' @@ -23,7 +22,7 @@ export default class Credentials extends Command { } static topic = 'pg' - protected isDefaultCredential(cred: NonAdvancedCredentialInfo): boolean { + protected isDefaultCredential(cred: CredentialInfo): boolean { return cred.name === 'default' } @@ -34,16 +33,16 @@ export default class Credentials extends Command { const dbResolver = new utils.pg.DatabaseResolver(this.heroku) const {addon} = await dbResolver.getAttachment(app, database) - const {data} = new HerokuSDK() - const credentials = await listCredentials(data, addon.id) + const {data} = new HerokuSDK({extensions: [postgresDatabaseExtensions]}) + const credentials = await data.postgresDatabase.listCredentials(app, addon.name) const sortedCredentials = this.sortByDefaultAndName(credentials) const {body: attachments} = await this.heroku.get[]>(`/addons/${addon.id}/addon-attachments`) - const presentCredential = (cred: NonAdvancedCredentialInfo): string => { + const presentCredential = (cred: CredentialInfo): string => { let credAttachments = [] as Required[] credAttachments = cred.name === 'default' ? attachments.filter(a => a.namespace === null) : attachments.filter(a => a.namespace === `credential:${cred.name}`) - return presentCredentialAttachments(app, credAttachments, sortedCredentials, cred.name) + return presentCredentialAttachments(app, credAttachments, sortedCredentials as any, cred.name) } hux.table(credentials, { @@ -56,8 +55,8 @@ export default class Credentials extends Command { }, huxTableNoWrapOptions(flags['no-wrap'])) } - protected sortByDefaultAndName(credentials: NonAdvancedCredentialInfo[]) { - return credentials.sort((a: NonAdvancedCredentialInfo, b: NonAdvancedCredentialInfo) => { + protected sortByDefaultAndName(credentials: CredentialInfo[]) { + return credentials.sort((a: CredentialInfo, b: CredentialInfo) => { const isDefaultA = this.isDefaultCredential(a) const isDefaultB = this.isDefaultCredential(b) diff --git a/src/commands/pg/upgrade/cancel.ts b/src/commands/pg/upgrade/cancel.ts index ff702440aa..dcf1fb2f8d 100644 --- a/src/commands/pg/upgrade/cancel.ts +++ b/src/commands/pg/upgrade/cancel.ts @@ -1,12 +1,11 @@ import {Command, flags} from '@heroku-cli/command' import {color, utils} from '@heroku/heroku-cli-util' import {HerokuSDK} from '@heroku/sdk' -import {DatabaseCancelUpgradeResult} from '@heroku/types/data' +import {databaseExtensions} from '@heroku/sdk/extensions/data' import {Args, ux} from '@oclif/core' import tsheredoc from 'tsheredoc' import ConfirmCommand from '../../../lib/confirm-command.js' -import {getDatabaseInfo} from '../../../lib/pg/sdk-adapter.js' import {formatResponseWithCommands} from '../../../lib/pg/util.js' import {nls} from '../../../nls.js' @@ -38,8 +37,8 @@ export default class Upgrade extends Command { if (utils.pg.isEssentialDatabase(db)) ux.error(`You can't use ${color.code('pg:upgrade:cancel')} on Essential-tier databases. You can only use this command on Standard-tier and higher leader databases.`) - const {data} = new HerokuSDK() - const replica = await getDatabaseInfo(data, db.id) + const {data} = new HerokuSDK({extensions: [databaseExtensions]}) + const replica = await data.database.describe(app, db.name) if (replica.following) ux.error(`You can't use ${color.code('pg:upgrade:cancel')} on follower databases. You can only use this command on Standard-tier and higher leader databases.`) @@ -52,7 +51,7 @@ export default class Upgrade extends Command { try { ux.action.start(`Cancelling upgrade on ${color.addon(db.name)}`) - const response: DatabaseCancelUpgradeResult = await data.database.cancelUpgrade(db.id) + const response = await data.database.cancelUpgrade(app, db.name) ux.action.stop('done\n' + formatResponseWithCommands(response.message)) } catch (error: any) { if (error.id && error.message) { diff --git a/src/commands/pg/upgrade/dryrun.ts b/src/commands/pg/upgrade/dryrun.ts index d8ece92a87..0bdac0592d 100644 --- a/src/commands/pg/upgrade/dryrun.ts +++ b/src/commands/pg/upgrade/dryrun.ts @@ -1,11 +1,11 @@ import {Command, flags} from '@heroku-cli/command' import {color, utils} from '@heroku/heroku-cli-util' import {HerokuSDK} from '@heroku/sdk' +import {databaseExtensions} from '@heroku/sdk/extensions/data' import {Args, ux} from '@oclif/core' import tsheredoc from 'tsheredoc' import ConfirmCommand from '../../../lib/confirm-command.js' -import {dryRunUpgrade, getDatabaseInfo} from '../../../lib/pg/sdk-adapter.js' import {formatResponseWithCommands} from '../../../lib/pg/util.js' import {nls} from '../../../nls.js' @@ -39,8 +39,8 @@ export default class Upgrade extends Command { ux.error(`You can't use ${color.code('pg:upgrade:dryrun')} on Essential-tier databases. You can only use this command on Standard-tier and higher leader databases.`) const versionPhrase = version ? heredoc(`Postgres version ${version}`) : heredoc('the latest supported Postgres version') - const {data} = new HerokuSDK() - const replica = await getDatabaseInfo(data, db.id) + const {data} = new HerokuSDK({extensions: [databaseExtensions]}) + const replica = await data.database.describe(app, db.name) if (replica.following) ux.error(`You can't use ${color.code('pg:upgrade:dryrun')} on follower databases. You can only use this command on Standard-tier and higher leader databases.`) @@ -50,7 +50,7 @@ export default class Upgrade extends Command { try { ux.action.start(`Starting a test upgrade on ${color.datastore(db.name)}`) - const response = await dryRunUpgrade(data, db.id, {version}) + const response = await data.database.dryRunUpgrade(app, db.name, {version}) ux.action.stop('done\n' + formatResponseWithCommands(response.message)) } catch (error: any) { if (error.id && error.message) { diff --git a/src/commands/pg/upgrade/prepare.ts b/src/commands/pg/upgrade/prepare.ts index cad74d19d2..994a42ada7 100644 --- a/src/commands/pg/upgrade/prepare.ts +++ b/src/commands/pg/upgrade/prepare.ts @@ -5,7 +5,7 @@ import {Args, ux} from '@oclif/core' import tsheredoc from 'tsheredoc' import ConfirmCommand from '../../../lib/confirm-command.js' -import {getDatabaseInfo, prepareUpgrade} from '../../../lib/pg/sdk-adapter.js' +import {databaseExtensions} from '@heroku/sdk/extensions/data' import {formatResponseWithCommands} from '../../../lib/pg/util.js' import {nls} from '../../../nls.js' @@ -39,8 +39,8 @@ export default class Upgrade extends Command { ux.error(`You can only use ${color.code('heroku pg:upgrade:prepare')} on Standard-tier and higher leader databases. For Essential-tier databases, use ${color.code('heroku pg:upgrade:run')} instead.`) const versionPhrase = version ? heredoc(`Postgres version ${version}`) : heredoc('the latest supported Postgres version') - const {data} = new HerokuSDK() - const replica = await getDatabaseInfo(data, db.id) + const {data} = new HerokuSDK({extensions: [databaseExtensions]}) + const replica = await data.database.describe(app, db.name) if (replica.following) ux.error(`You can only use ${color.code('heroku pg:upgrade:prepare')} on Standard-tier and higher leader databases. For follower databases, use ${color.code('heroku pg:upgrade:run')} instead.`) @@ -52,7 +52,7 @@ export default class Upgrade extends Command { try { ux.action.start(`Preparing upgrade on ${color.addon(db.name)}`) - const response = await prepareUpgrade(data, db.id, {version}) + const response = await data.database.prepareUpgrade(app, db.name, {version}) ux.action.stop(heredoc(`done\n${formatResponseWithCommands(response.message)}`)) } catch (error: any) { if (error.id && error.message) { diff --git a/src/commands/pg/upgrade/run.ts b/src/commands/pg/upgrade/run.ts index 4aeb49c017..02bb0b5a27 100644 --- a/src/commands/pg/upgrade/run.ts +++ b/src/commands/pg/upgrade/run.ts @@ -6,7 +6,7 @@ import {Args, ux} from '@oclif/core' import tsheredoc from 'tsheredoc' import ConfirmCommand from '../../../lib/confirm-command.js' -import {getDatabaseInfo, runUpgrade} from '../../../lib/pg/sdk-adapter.js' +import {databaseExtensions} from '@heroku/sdk/extensions/data' import {databaseNameFromUrl, formatResponseWithCommands} from '../../../lib/pg/util.js' import {nls} from '../../../nls.js' @@ -58,8 +58,8 @@ export default class Upgrade extends Command { ux.error(`You can only use ${color.code('pg:upgrade:*')} commands on Essential-* and higher plans.`) const versionPhrase = version ? heredoc(`Postgres version ${version}`) : heredoc('the latest supported Postgres version') - const {data} = new HerokuSDK() - const replica = await getDatabaseInfo(data, db.id) + const {data} = new HerokuSDK({extensions: [databaseExtensions]}) + const replica = await data.database.describe(app, db.name) if (utils.pg.isEssentialDatabase(db)) { await new ConfirmCommand().confirm(app, confirm, heredoc(` @@ -89,7 +89,7 @@ export default class Upgrade extends Command { try { ux.action.start(`Starting upgrade on ${color.datastore(db.name)}`) - const response = await runUpgrade(data, db.id, {version}) + const response = await data.database.runUpgrade(app, db.name, {version}) ux.action.stop(heredoc(`done\n${formatResponseWithCommands(response.message)}`)) } catch (error: any) { if (error.id && error.message) { diff --git a/src/commands/pg/upgrade/wait.ts b/src/commands/pg/upgrade/wait.ts index 0c1841c90a..4792245f0e 100644 --- a/src/commands/pg/upgrade/wait.ts +++ b/src/commands/pg/upgrade/wait.ts @@ -6,8 +6,8 @@ import debug from 'debug' import tsheredoc from 'tsheredoc' import notify from '../../../lib/notify.js' -import {getUpgradeWaitStatus} from '../../../lib/pg/sdk-adapter.js' -import {PgUpgradeStatus} from '../../../lib/pg/types.js' +import {databaseExtensions} from '@heroku/sdk/extensions/data' +import type {DatabaseUpgradeWaitResult} from '@heroku/sdk/resources/data/database' import {formatResponseWithCommands} from '../../../lib/pg/util.js' import {nls} from '../../../nls.js' @@ -50,17 +50,17 @@ export default class Wait extends Command { const dbName = args.database const pgDebug = debug('pg') - const {data} = new HerokuSDK() + const {data} = new HerokuSDK({extensions: [databaseExtensions]}) const waitFor = async (db: pg.ExtendedAddonAttachment['addon']) => { const interval = (!waitInterval || waitInterval < 0) ? 5 : waitInterval - let status: PgUpgradeStatus + let status: DatabaseUpgradeWaitResult let waiting = false let retries = 20 const notFoundMessage = 'Waiting to provision...' while (true) { try { - status = await getUpgradeWaitStatus(data, db.id) + status = await data.database.upgradeWaitStatus(app, db.name) } catch (error: any) { if (!retries || error.statusCode !== 404) { pgDebug(error) diff --git a/src/commands/pipelines/create.ts b/src/commands/pipelines/create.ts index 25e7318fa3..2be4ba0fcb 100644 --- a/src/commands/pipelines/create.ts +++ b/src/commands/pipelines/create.ts @@ -1,7 +1,7 @@ import {Command, flags} from '@heroku-cli/command' import {StageCompletion} from '@heroku-cli/command/lib/completions.js' import * as color from '@heroku/heroku-cli-util/color' -import {createPlatformClient} from '@heroku/sdk/platform' +import {HerokuSDK} from '@heroku/sdk' import {PipelineCreateOpts} from '@heroku/types/3.sdk' import {Args, ux} from '@oclif/core' import {type Answers, type InputQuestion, type ListQuestion} from 'inquirer' @@ -84,12 +84,12 @@ export default class Create extends Command { const ownerType = teamName ? 'team' : 'user' - const heroku = createPlatformClient() + const {platform} = new HerokuSDK() // If team or org is not specified, we assign ownership to the user creating const ownerRecord = teamName - ? await heroku.team.info(teamName) - : await heroku.account.infoByUser('~') + ? await platform.team.info(teamName) + : await platform.account.infoByUser('~') const ownerID = ownerRecord.id! const answers: Answers = await inquirer.prompt(questions) @@ -103,11 +103,11 @@ export default class Create extends Command { name, owner: {id: ownerID, type: ownerType}, } - const pipeline = await heroku.pipeline.create(body) + const pipeline = await platform.pipeline.create(body) ux.action.stop() ux.action.start(`Adding ${color.app(app)} to ${color.pipeline(pipeline.name || '')} pipeline as ${stage}`) - await heroku.pipelineCoupling.create({ + await platform.pipelineCoupling.create({ app, pipeline: pipeline.id!, stage, diff --git a/src/commands/pipelines/destroy.ts b/src/commands/pipelines/destroy.ts index 28b7cf8cae..c3aeb538a0 100644 --- a/src/commands/pipelines/destroy.ts +++ b/src/commands/pipelines/destroy.ts @@ -1,7 +1,7 @@ import {Command} from '@heroku-cli/command' import * as Heroku from '@heroku-cli/schema' import * as color from '@heroku/heroku-cli-util/color' -import {createPlatformClient} from '@heroku/sdk/platform' +import {HerokuSDK} from '@heroku/sdk' import {Args, ux} from '@oclif/core' import disambiguate from '../../lib/pipelines/disambiguate.js' @@ -23,8 +23,8 @@ export default class PipelinesDestroy extends Command { const pipeline: Heroku.Pipeline = await disambiguate(this.heroku, args.pipeline) ux.action.start(`Destroying ${color.pipeline(pipeline.name!)} pipeline`) - const heroku = createPlatformClient() - await heroku.pipeline.delete(pipeline.id!) + const {platform} = new HerokuSDK() + await platform.pipeline.delete(pipeline.id!) ux.action.stop() } } diff --git a/src/commands/pipelines/index.ts b/src/commands/pipelines/index.ts index e49e5229de..45a7550c09 100644 --- a/src/commands/pipelines/index.ts +++ b/src/commands/pipelines/index.ts @@ -1,6 +1,6 @@ import {Command, flags} from '@heroku-cli/command' import {color, hux} from '@heroku/heroku-cli-util' -import {createPlatformClient} from '@heroku/sdk/platform' +import {HerokuSDK} from '@heroku/sdk' import {ux} from '@oclif/core/ux' export default class Pipelines extends Command { @@ -15,8 +15,8 @@ export default class Pipelines extends Command { async run() { const {flags} = await this.parse(Pipelines) - const heroku = createPlatformClient() - const pipelines = await heroku.pipeline.list() + const {platform} = new HerokuSDK() + const pipelines = await platform.pipeline.list() if (flags.json) { hux.styledJSON(pipelines) diff --git a/src/commands/pipelines/promote.ts b/src/commands/pipelines/promote.ts index 6cbf9ea764..b894510703 100644 --- a/src/commands/pipelines/promote.ts +++ b/src/commands/pipelines/promote.ts @@ -1,3 +1,4 @@ +import type {PipelinePromotionTarget} from '@heroku/types/3.sdk' import type {AppWithPipelineCoupling} from '@heroku/sdk/resources/platform/pipeline-coupling' import {APIClient, Command, flags} from '@heroku-cli/command' @@ -118,12 +119,12 @@ export default class Promote extends Command { const appsByID = keyBy(allApps, 'id') - const styledTargets = promotionTargets.reduce((memo: Heroku.App, target: any) => { + const styledTargets = promotionTargets.reduce((memo: Record, target: PipelinePromotionTarget) => { const app = appsByID[target.app.id] - const details = [target.status] + const details: string[] = [target.status] if (isFailed(target)) { - details.push(target.error_message) + details.push(target.error_message ?? '') } memo[app.name] = details @@ -158,10 +159,10 @@ async function getCoupling(heroku: APIClient, app: string): Promise { - return data.database.info(addonId) as unknown as PgDatabase -} - -export async function getUpgradeWaitStatus(data: DataClient, addonId: string): Promise { - return data.database.upgradeWaitStatus(addonId) as unknown as PgUpgradeStatus -} - -export async function listTransfersByApp(data: DataClient, appIdentity: string): Promise { - return data.transfer.listByApp(appIdentity) as unknown as BackupTransfer[] -} - -export async function listCredentials(data: DataClient, addonId: string): Promise { - return data.postgresDatabase.listCredentials(addonId) as unknown as NonAdvancedCredentialInfo[] -} - -export async function runUpgrade(data: DataClient, addonId: string, body: {version?: string}): Promise<{message: string}> { - const fn = data.database.runUpgrade as (name: string, body: {version?: string}) => Promise - return fn(addonId, body) as Promise<{message: string}> -} - -export async function prepareUpgrade(data: DataClient, addonId: string, body: {version?: string}): Promise<{message: string}> { - const fn = data.database.prepareUpgrade as (name: string, body: {version?: string}) => Promise - return fn(addonId, body) as Promise<{message: string}> -} - -export async function dryRunUpgrade(data: DataClient, addonId: string, body: {version?: string}): Promise<{message: string}> { - const fn = data.database.dryRunUpgrade as (name: string, body: {version?: string}) => Promise - return fn(addonId, body) as Promise<{message: string}> -} diff --git a/src/lib/pipelines/disambiguate.ts b/src/lib/pipelines/disambiguate.ts index 5361a943db..2552e7b6fa 100644 --- a/src/lib/pipelines/disambiguate.ts +++ b/src/lib/pipelines/disambiguate.ts @@ -1,6 +1,6 @@ import {APIClient} from '@heroku-cli/command' import * as Heroku from '@heroku-cli/schema' -import {createPlatformClient} from '@heroku/sdk/platform' +import {HerokuSDK} from '@heroku/sdk' import inquirer from 'inquirer' import {findPipelineByName} from '../api.js' @@ -10,8 +10,8 @@ export default async function disambiguate(heroku: APIClient, pipelineIDOrName: let pipeline: Heroku.Pipeline if (uuidValidate(pipelineIDOrName)) { - const sdk = createPlatformClient() - pipeline = await sdk.pipeline.info(pipelineIDOrName) + const {platform} = new HerokuSDK() + pipeline = await platform.pipeline.info(pipelineIDOrName) } else { const {body: pipelines} = await findPipelineByName(heroku, pipelineIDOrName) diff --git a/src/lib/run/log-displayer.ts b/src/lib/run/log-displayer.ts index fcdf50b9d0..4d1a869d8e 100644 --- a/src/lib/run/log-displayer.ts +++ b/src/lib/run/log-displayer.ts @@ -16,15 +16,18 @@ export interface LogDisplayerOptions { // Install once at module load so repeated displayLogs() calls don't // stack listeners on process.stdout (which would trip Node's -// MaxListeners warning). -process.stdout.on('error', err => { - if (err.code === 'EPIPE') { - // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit - process.exit(0) - } else { - ux.error(err.message ?? String(err), {exit: 1}) - } -}) +// MaxListeners warning). Skip in test environments to avoid killing +// the test runner on piped-output scenarios. +if (!process.env.IS_HEROKU_TEST_ENV) { + process.stdout.on('error', err => { + if (err.code === 'EPIPE') { + // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit + process.exit(0) + } else { + ux.error(err.message ?? String(err), {exit: 1}) + } + }) +} export async function displayLogs(options: LogDisplayerOptions): Promise { const controller = new AbortController() diff --git a/test/helpers/mock-sdk.ts b/test/helpers/mock-sdk.ts index 8b448d6dbb..6a8dce8ce9 100644 --- a/test/helpers/mock-sdk.ts +++ b/test/helpers/mock-sdk.ts @@ -2,11 +2,13 @@ import {HerokuSDK} from '@heroku/sdk' import {SinonStub, stub} from 'sinon' type DeepPartial = { - [P in keyof T]?: T[P] extends object ? DeepPartial : T[P] + [P in keyof T]?: T[P] extends (...args: any[]) => any ? T[P] : T[P] extends object ? DeepPartial : T[P] } -type StubbedDataClient = DeepPartial -type StubbedPlatformClient = DeepPartial +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type StubbedDataClient = Record +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type StubbedPlatformClient = Record export interface MockSDK { dataStub?: SinonStub diff --git a/test/unit/commands/addons/create.unit.test.ts b/test/unit/commands/addons/create.unit.test.ts index cfc3becd5a..28cd318120 100644 --- a/test/unit/commands/addons/create.unit.test.ts +++ b/test/unit/commands/addons/create.unit.test.ts @@ -245,7 +245,7 @@ describe('addons:create', function () { it('notifies when provisioning failure occurs', async function () { const notifySpy = sandbox.spy(Cmd, 'notifier') const deprovisionedAddon = {..._.clone(addon), state: 'deprovisioned'} - const createAndWaitStub = stub().rejects(new AddonProvisioningFailedError(deprovisionedAddon)) + const createAndWaitStub = stub().rejects(new AddonProvisioningFailedError(deprovisionedAddon as any)) sdkMock = mockSDKPlatform({addOn: {createAndWait: createAndWaitStub}}) try { @@ -269,7 +269,7 @@ describe('addons:create', function () { it('shows that it failed to provision', async function () { const deprovisionedAddon = _.clone(addon) deprovisionedAddon.state = 'deprovisioned' - const createAndWaitStub = stub().rejects(new AddonProvisioningFailedError(deprovisionedAddon)) + const createAndWaitStub = stub().rejects(new AddonProvisioningFailedError(deprovisionedAddon as any)) sdkMock = mockSDKPlatform({addOn: {createAndWait: createAndWaitStub}}) const {error} = await runCommand(Cmd, [ diff --git a/test/unit/commands/addons/upgrade.unit.test.ts b/test/unit/commands/addons/upgrade.unit.test.ts index fad36c4212..b7da191db6 100644 --- a/test/unit/commands/addons/upgrade.unit.test.ts +++ b/test/unit/commands/addons/upgrade.unit.test.ts @@ -208,7 +208,7 @@ describe('addons:upgrade', function () { }) it('displays an error when multiple matches exist', async function () { - const upgradeStub = stub().rejects(new AddonAmbiguousError([{name: 'addon-1'}, {name: 'addon-2'}])) + const upgradeStub = stub().rejects(new AddonAmbiguousError([{name: 'addon-1'}, {name: 'addon-2'}] as any)) sdkMock = mockSDKPlatform({addOn: {listPlans: stub().resolves([]), upgrade: upgradeStub}}) try { await runCommand(Cmd, [ @@ -224,7 +224,7 @@ describe('addons:upgrade', function () { }) it('handles multiple add-ons', async function () { - const upgradeStub = stub().rejects(new AddonAmbiguousError([{name: 'db1-swiftly-123'}, {name: 'db1-swiftly-456'}])) + const upgradeStub = stub().rejects(new AddonAmbiguousError([{name: 'db1-swiftly-123'}, {name: 'db1-swiftly-456'}] as any)) sdkMock = mockSDKPlatform({addOn: {listPlans: stub().resolves([]), upgrade: upgradeStub}}) try { await runCommand(Cmd, [ diff --git a/test/unit/commands/pg/backups/index.unit.test.ts b/test/unit/commands/pg/backups/index.unit.test.ts index 7bfb087c2a..4f776bb611 100644 --- a/test/unit/commands/pg/backups/index.unit.test.ts +++ b/test/unit/commands/pg/backups/index.unit.test.ts @@ -1,30 +1,23 @@ import {runCommand} from '@heroku-cli/test-utils' import {expect} from 'chai' -import {stub} from 'sinon' +import {restore, stub} from 'sinon' import tsheredoc from 'tsheredoc' import type {BackupTransfer} from '../../../../../src/lib/pg/types.js' import Cmd from '../../../../../src/commands/pg/backups/index.js' -import {mockSDKData, MockSDK} from '../../../../helpers/mock-sdk.js' +import {type MockSDK, mockSDKData} from '../../../../helpers/mock-sdk.js' import normalizeTableOutput from '../../../../helpers/utils/normalize-table-output.js' const heredoc = tsheredoc.default describe('pg:backups', function () { let sdkMock: MockSDK - let listByAppStub: ReturnType let transfers: BackupTransfer[] - beforeEach(function () { - listByAppStub = stub().resolves(transfers) - sdkMock = mockSDKData({ - transfer: {listByApp: listByAppStub}, - }) - }) - afterEach(function () { sdkMock.restore() + restore() }) describe('with no backups/restores/copies', function () { @@ -33,7 +26,7 @@ describe('pg:backups', function () { }) it('shows empty message', async function () { - listByAppStub.resolves(transfers) + sdkMock = mockSDKData({transfer: {listByApp: stub().resolves(transfers)}}) const {stdout} = await runCommand(Cmd, [ '--app', 'myapp', @@ -108,7 +101,7 @@ describe('pg:backups', function () { }) it('shows backups', async function () { - listByAppStub.resolves(transfers) + sdkMock = mockSDKData({transfer: {listByApp: stub().resolves(transfers)}}) const {stdout} = await runCommand(Cmd, [ '--app', 'myapp', @@ -153,7 +146,7 @@ No copies found. Use heroku pg:copy to copy a database to another }) it('shows restore', async function () { - listByAppStub.resolves(transfers) + sdkMock = mockSDKData({transfer: {listByApp: stub().resolves(transfers)}}) const {stdout} = await runCommand(Cmd, [ '--app', 'myapp', @@ -196,7 +189,7 @@ No copies found. Use heroku pg:copy to copy a database to another }) it('shows copy', async function () { - listByAppStub.resolves(transfers) + sdkMock = mockSDKData({transfer: {listByApp: stub().resolves(transfers)}}) const {stdout} = await runCommand(Cmd, [ '--app', 'myapp', diff --git a/test/unit/commands/pg/credentials.unit.test.ts b/test/unit/commands/pg/credentials.unit.test.ts index 362fbc1c00..191f7ec472 100644 --- a/test/unit/commands/pg/credentials.unit.test.ts +++ b/test/unit/commands/pg/credentials.unit.test.ts @@ -19,96 +19,91 @@ describe('pg:credentials', function () { name: 'postgres-1', plan: {name: 'heroku-postgresql:standard-0'}, } + + const credentials = [ + { + credentials: [], + database: 'd123', + host: 'localhost', + name: 'ransom', + port: 5442, + state: 'active', + uuid: 'aaaa', + }, + { + credentials: [], + database: 'd123', + host: 'localhost', + name: 'default', + port: 5442, + state: 'active', + uuid: 'aaab', + }, + { + credentials: [ + { + connections: 0, + state: 'revoking', + user: 'jeff', + }, + { + connections: 2, + state: 'active', + user: 'jeff-rotating', + }, + ], + database: 'd123', + host: 'localhost', + name: 'jeff', + port: 5442, + state: 'rotating', + uuid: 'aabb', + }, + ] + + const attachments = [ + { + app: {name: 'main-app'}, + name: 'DATABASE', + namespace: null, + }, + { + app: {name: 'main-app'}, + name: 'HEROKU_POSTGRESQL_GREEN', + namespace: 'credential:jeff', + }, + { + app: {name: 'another-app'}, + name: 'HEROKU_POSTGRESQL_PINK', + namespace: 'credential:jeff', + }, + { + app: {name: 'yet-another-app'}, + name: 'HEROKU_POSTGRESQL_BLUE', + namespace: 'credential:ransom', + }, + ] + let api: nock.Scope let sdkMock: MockSDK - let listCredentialsStub: ReturnType beforeEach(function () { api = nock('https://api.heroku.com') - listCredentialsStub = stub() - sdkMock = mockSDKData({ - postgresDatabase: { - listCredentials: listCredentialsStub, - }, - }) }) afterEach(function () { nock.cleanAll() - restore() sdkMock.restore() + restore() api.done() }) it('shows the correct credentials', async function () { - const credentials = [ - { - credentials: [], - database: 'd123', - host: 'localhost', - name: 'ransom', - port: 5442, - state: 'active', - uuid: 'aaaa', - }, - { - credentials: [], - database: 'd123', - host: 'localhost', - name: 'default', - port: 5442, - state: 'active', - uuid: 'aaab', - }, - { - credentials: [ - { - connections: 0, - state: 'revoking', - user: 'jeff', - }, - { - connections: 2, - state: 'active', - user: 'jeff-rotating', - }, - ], - database: 'd123', - host: 'localhost', - name: 'jeff', - port: 5442, - state: 'rotating', - uuid: 'aabb', - }, - ] - const attachments = [ - { - app: {name: 'main-app'}, - name: 'DATABASE', - namespace: null, - }, - { - app: {name: 'main-app'}, - name: 'HEROKU_POSTGRESQL_GREEN', - namespace: 'credential:jeff', - }, - { - app: {name: 'another-app'}, - name: 'HEROKU_POSTGRESQL_PINK', - namespace: 'credential:jeff', - }, - { - app: {name: 'yet-another-app'}, - name: 'HEROKU_POSTGRESQL_BLUE', - namespace: 'credential:ransom', - }, - ] - api.post('/actions/addon-attachments/resolve', {addon_attachment: 'DATABASE_URL', app: 'myapp'}) .reply(200, [{addon}]) .get('/addons/1/addon-attachments') .reply(200, attachments) - listCredentialsStub.resolves(credentials) + sdkMock = mockSDKData({postgresDatabase: {listCredentials: stub().resolves(credentials)}}) const {stdout} = await runCommand(Cmd, [ '--app', @@ -132,7 +127,7 @@ describe('pg:credentials', function () { }) it('shows the correct rotation information if no connection information is available yet', async function () { - const credentials = [ + const rotatingCredentials = [ { credentials: [], database: 'd123', @@ -170,34 +165,12 @@ describe('pg:credentials', function () { uuid: 'aabb', }, ] - const attachments = [ - { - app: {name: 'main-app'}, - name: 'DATABASE', - namespace: null, - }, - { - app: {name: 'main-app'}, - name: 'HEROKU_POSTGRESQL_GREEN', - namespace: 'credential:jeff', - }, - { - app: {name: 'another-app'}, - name: 'HEROKU_POSTGRESQL_PINK', - namespace: 'credential:jeff', - }, - { - app: {name: 'yet-another-app'}, - name: 'HEROKU_POSTGRESQL_BLUE', - namespace: 'credential:ransom', - }, - ] api.post('/actions/addon-attachments/resolve', {addon_attachment: 'DATABASE_URL', app: 'myapp'}) .reply(200, [{addon}]) .get('/addons/1/addon-attachments') .reply(200, attachments) - listCredentialsStub.resolves(credentials) + sdkMock = mockSDKData({postgresDatabase: {listCredentials: stub().resolves(rotatingCredentials)}}) const {stdout} = await runCommand(Cmd, [ '--app', @@ -218,20 +191,20 @@ describe('pg:credentials', function () { }) it('passes no-wrap option through to table rendering', async function () { - const credentials = [ + const simpleCredentials = [ { credentials: [], database: 'd123', host: 'localhost', name: 'default', port: 5442, state: 'active', uuid: 'aaaa', }, ] - const attachments = [ + const simpleAttachments = [ {app: {name: 'main-app'}, name: 'DATABASE', namespace: null}, ] api.post('/actions/addon-attachments/resolve', {addon_attachment: 'DATABASE_URL', app: 'myapp'}) .reply(200, [{addon}]) .get('/addons/1/addon-attachments') - .reply(200, attachments) - listCredentialsStub.resolves(credentials) + .reply(200, simpleAttachments) + sdkMock = mockSDKData({postgresDatabase: {listCredentials: stub().resolves(simpleCredentials)}}) const tableStub = stub(hux, 'table') await runCommand(Cmd, ['--app', 'myapp', '--no-wrap']) diff --git a/test/unit/commands/pg/upgrade/cancel.unit.test.ts b/test/unit/commands/pg/upgrade/cancel.unit.test.ts index cbc0857af5..3c52204176 100644 --- a/test/unit/commands/pg/upgrade/cancel.unit.test.ts +++ b/test/unit/commands/pg/upgrade/cancel.unit.test.ts @@ -38,7 +38,7 @@ describe('pg:upgrade:cancel', function () { infoStub = stub() cancelUpgradeStub = stub() sdkMock = mockSDKData({ - database: {info: infoStub, cancelUpgrade: cancelUpgradeStub}, + database: {describe: infoStub, cancelUpgrade: cancelUpgradeStub}, }) }) diff --git a/test/unit/commands/pg/upgrade/dryrun.unit.test.ts b/test/unit/commands/pg/upgrade/dryrun.unit.test.ts index b583cc5d0f..64e02106cd 100644 --- a/test/unit/commands/pg/upgrade/dryrun.unit.test.ts +++ b/test/unit/commands/pg/upgrade/dryrun.unit.test.ts @@ -38,7 +38,7 @@ describe('pg:upgrade:dryrun', function () { infoStub = stub() dryRunUpgradeStub = stub() sdkMock = mockSDKData({ - database: {info: infoStub, dryRunUpgrade: dryRunUpgradeStub}, + database: {describe: infoStub, dryRunUpgrade: dryRunUpgradeStub}, }) }) diff --git a/test/unit/commands/pg/upgrade/prepare.unit.test.ts b/test/unit/commands/pg/upgrade/prepare.unit.test.ts index 592e5785e3..5dea96ddbe 100644 --- a/test/unit/commands/pg/upgrade/prepare.unit.test.ts +++ b/test/unit/commands/pg/upgrade/prepare.unit.test.ts @@ -38,7 +38,7 @@ describe('pg:upgrade:prepare', function () { infoStub = stub() prepareUpgradeStub = stub() sdkMock = mockSDKData({ - database: {info: infoStub, prepareUpgrade: prepareUpgradeStub}, + database: {describe: infoStub, prepareUpgrade: prepareUpgradeStub}, }) }) diff --git a/test/unit/commands/pg/upgrade/run.unit.test.ts b/test/unit/commands/pg/upgrade/run.unit.test.ts index ec1cf2693d..ba5838fcc6 100644 --- a/test/unit/commands/pg/upgrade/run.unit.test.ts +++ b/test/unit/commands/pg/upgrade/run.unit.test.ts @@ -40,7 +40,7 @@ describe('pg:upgrade:run', function () { infoStub = stub() runUpgradeStub = stub() sdkMock = mockSDKData({ - database: {info: infoStub, runUpgrade: runUpgradeStub}, + database: {describe: infoStub, runUpgrade: runUpgradeStub}, }) }) diff --git a/test/unit/commands/pipelines/create.unit.test.ts b/test/unit/commands/pipelines/create.unit.test.ts index 43fe7ca244..4b96ea2fa4 100644 --- a/test/unit/commands/pipelines/create.unit.test.ts +++ b/test/unit/commands/pipelines/create.unit.test.ts @@ -1,11 +1,14 @@ import {runCommand} from '@heroku-cli/test-utils' import {expect} from 'chai' import nock from 'nock' +import {restore, stub} from 'sinon' import PipelinesCreate from '../../../../src/commands/pipelines/create.js' +import {type MockSDK, mockSDKPlatform} from '../../../helpers/mock-sdk.js' describe('pipelines:create', function () { let api: nock.Scope + let sdkMock: MockSDK beforeEach(function () { api = nock('https://api.heroku.com') @@ -14,30 +17,26 @@ describe('pipelines:create', function () { afterEach(function () { api.done() nock.cleanAll() + sdkMock.restore() + restore() }) describe('successful pipeline creation', function () { context('when not specifying ownership', function () { it('creates a pipeline with default user ownership', async function () { - api - .post('/pipeline-couplings') - .reply(201, {id: '0123', stage: 'production'}) - .get('/users/~') - .reply(200, {id: '1234-567'}) - .post('/pipelines', { - generation: {name: 'fir'}, - name: 'example-pipeline', - owner: {id: '1234-567', type: 'user'}, - }) - .reply(201, { - id: '0123', - name: 'example-pipeline', - owner: {id: '1234-567', type: 'user'}, - }) + const pipeline = {id: '0123', name: 'example-pipeline', owner: {id: '1234-567', type: 'user'}} - nock('https://api.heroku.com') - .get('/apps/example-app') - .reply(200, {generation: 'fir', id: '0123', name: 'example-app'}) + // getGenerationByAppId still uses this.heroku + api.get('/apps/example-app').reply(200, {generation: 'fir', id: '0123', name: 'example-app'}) + + const infoByUserStub = stub().resolves({id: '1234-567'}) + const pipelineCreateStub = stub().resolves(pipeline) + const couplingCreateStub = stub().resolves({id: '0123', stage: 'production'}) + sdkMock = mockSDKPlatform({ + account: {infoByUser: infoByUserStub}, + pipeline: {create: pipelineCreateStub}, + pipelineCoupling: {create: couplingCreateStub}, + }) const {stderr, stdout} = await runCommand(PipelinesCreate, [ '--app', @@ -50,30 +49,27 @@ describe('pipelines:create', function () { expect(stdout).to.equal('') expect(stderr).to.contain('Creating example-pipeline pipeline... done') expect(stderr).to.contain('Adding ⬢ example-app to example-pipeline pipeline as production... done') + expect(infoByUserStub.calledOnceWith('~')).to.be.true + expect(pipelineCreateStub.calledOnce).to.be.true + expect(pipelineCreateStub.firstCall.args[0]).to.deep.include({name: 'example-pipeline', owner: {id: '1234-567', type: 'user'}}) + expect(couplingCreateStub.calledOnce).to.be.true }) }) context('when specifying a team as owner', function () { it('creates a pipeline with team ownership', async function () { - nock('https://api.heroku.com') - .post('/pipeline-couplings') - .reply(201, {id: '0123', stage: 'production'}) - .get('/teams/my-team') - .reply(200, {id: '89-0123-456'}) - .post('/pipelines', { - generation: {name: 'fir'}, - name: 'example-pipeline', - owner: {id: '89-0123-456', type: 'team'}, - }) - .reply(201, { - id: '0123', - name: 'example-pipeline', - owner: {id: '89-0123-456', type: 'team'}, - }) + const pipeline = {id: '0123', name: 'example-pipeline', owner: {id: '89-0123-456', type: 'team'}} + + api.get('/apps/example-app').reply(200, {generation: 'fir', id: '0123', name: 'example-app'}) - nock('https://api.heroku.com') - .get('/apps/example-app') - .reply(200, {generation: 'fir', id: '0123', name: 'example-app'}) + const teamInfoStub = stub().resolves({id: '89-0123-456'}) + const pipelineCreateStub = stub().resolves(pipeline) + const couplingCreateStub = stub().resolves({id: '0123', stage: 'production'}) + sdkMock = mockSDKPlatform({ + pipeline: {create: pipelineCreateStub}, + pipelineCoupling: {create: couplingCreateStub}, + team: {info: teamInfoStub}, + }) const {stderr, stdout} = await runCommand(PipelinesCreate, [ '--app', @@ -88,6 +84,8 @@ describe('pipelines:create', function () { expect(stdout).to.equal('') expect(stderr).to.contain('Creating example-pipeline pipeline... done') expect(stderr).to.contain('Adding ⬢ example-app to example-pipeline pipeline as production... done') + expect(teamInfoStub.calledOnceWith('my-team')).to.be.true + expect(pipelineCreateStub.firstCall.args[0]).to.deep.include({name: 'example-pipeline', owner: {id: '89-0123-456', type: 'team'}}) }) }) }) diff --git a/test/unit/commands/pipelines/destroy.unit.test.ts b/test/unit/commands/pipelines/destroy.unit.test.ts index dd86be3f08..f1c9ccef45 100644 --- a/test/unit/commands/pipelines/destroy.unit.test.ts +++ b/test/unit/commands/pipelines/destroy.unit.test.ts @@ -1,11 +1,14 @@ import {runCommand} from '@heroku-cli/test-utils' import {expect} from 'chai' import nock from 'nock' +import {restore, stub} from 'sinon' import PipelinesDestroy from '../../../../src/commands/pipelines/destroy.js' +import {type MockSDK, mockSDKPlatform} from '../../../helpers/mock-sdk.js' describe('pipelines:destroy', function () { let api: nock.Scope + let sdkMock: MockSDK beforeEach(function () { api = nock('https://api.heroku.com') @@ -14,6 +17,8 @@ describe('pipelines:destroy', function () { afterEach(function () { api.done() nock.cleanAll() + sdkMock.restore() + restore() }) it('displays the right messages', async function () { @@ -22,11 +27,13 @@ describe('pipelines:destroy', function () { api .get(`/pipelines?eq[name]=${pipeline.name}`) .reply(200, [pipeline]) - .delete(`/pipelines/${pipeline.id}`) - .reply(200, pipeline) + + const deleteStub = stub().resolves(pipeline) + sdkMock = mockSDKPlatform({pipeline: {delete: deleteStub}}) const {stderr} = await runCommand(PipelinesDestroy, ['example']) expect(stderr).to.include('Destroying example pipeline... done') + expect(deleteStub.calledOnceWith(pipeline.id)).to.be.true }) }) diff --git a/test/unit/commands/pipelines/index.unit.test.ts b/test/unit/commands/pipelines/index.unit.test.ts index 100abe6663..66839f1b5a 100644 --- a/test/unit/commands/pipelines/index.unit.test.ts +++ b/test/unit/commands/pipelines/index.unit.test.ts @@ -1,28 +1,24 @@ import {runCommand} from '@heroku-cli/test-utils' import {expect} from 'chai' -import nock from 'nock' +import {restore, stub} from 'sinon' import Pipelines from '../../../../src/commands/pipelines/index.js' +import {type MockSDK, mockSDKPlatform} from '../../../helpers/mock-sdk.js' describe('pipelines', function () { - let api: nock.Scope - - beforeEach(function () { - api = nock('https://api.heroku.com') - }) + let sdkMock: MockSDK afterEach(function () { - api.done() - nock.cleanAll() + sdkMock.restore() + restore() }) it('shows a list of pipelines', async function () { - api - .get('/pipelines') - .reply(200, [ - {id: '0123', name: 'Betelgeuse'}, - {id: '9876', name: 'Sirius'}, - ]) + const listStub = stub().resolves([ + {id: '0123', name: 'Betelgeuse'}, + {id: '9876', name: 'Sirius'}, + ]) + sdkMock = mockSDKPlatform({pipeline: {list: listStub}}) const {stderr, stdout} = await runCommand(Pipelines, []) @@ -33,19 +29,16 @@ describe('pipelines', function () { }) it('shows a list of pipelines, json formatted', async function () { - api - .get('/pipelines') - .reply(200, [ - {id: '0123', name: 'Betelgeuse'}, - {id: '9876', name: 'Sirius'}, - ]) + const pipelines = [ + {id: '0123', name: 'Betelgeuse'}, + {id: '9876', name: 'Sirius'}, + ] + const listStub = stub().resolves(pipelines) + sdkMock = mockSDKPlatform({pipeline: {list: listStub}}) const {stderr, stdout} = await runCommand(Pipelines, ['--json']) expect(stderr).to.contain('') - expect(JSON.parse(stdout)).to.eql([ - {id: '0123', name: 'Betelgeuse'}, - {id: '9876', name: 'Sirius'}, - ]) + expect(JSON.parse(stdout)).to.eql(pipelines) }) }) diff --git a/test/unit/commands/pipelines/promote.unit.test.ts b/test/unit/commands/pipelines/promote.unit.test.ts index 5d0a48320c..ff9f7dc55d 100644 --- a/test/unit/commands/pipelines/promote.unit.test.ts +++ b/test/unit/commands/pipelines/promote.unit.test.ts @@ -163,7 +163,7 @@ describe('pipelines:promote', function () { const promoteStub = stub(Cmd, 'promotePipeline').callsFake(async (_ctx, _body, options) => { await options!.onReleaseStream!({ stream: streamBody, - target: {app: {id: targetApp1.id}, status: 'pending'}, + target: {app: {id: targetApp1.id}, error_message: null, id: 'target-1', pipeline_promotion: {id: 'promo-1'}, release: {id: 'release-1'}, status: 'pending'}, }) return { promotion, diff --git a/test/unit/commands/pipelines/transfer.unit.test.ts b/test/unit/commands/pipelines/transfer.unit.test.ts index cd885789a4..6dbae6cb86 100644 --- a/test/unit/commands/pipelines/transfer.unit.test.ts +++ b/test/unit/commands/pipelines/transfer.unit.test.ts @@ -5,6 +5,7 @@ import nock from 'nock' import {restore, stub} from 'sinon' import TransferCommand from '../../../../src/commands/pipelines/transfer.js' +import {type MockSDK, mockSDKPlatform} from '../../../helpers/mock-sdk.js' describe('pipelines:transfer', function () { const pipeline = { @@ -31,9 +32,11 @@ describe('pipelines:transfer', function () { const app = { id: coupling.app.id, name: 'my-app', + pipelineCoupling: coupling, } let api: nock.Scope + let sdkMock: MockSDK beforeEach(function () { api = nock('https://api.heroku.com') @@ -42,23 +45,23 @@ describe('pipelines:transfer', function () { afterEach(function () { api.done() nock.cleanAll() + sdkMock.restore() restore() }) - function setupCommonMocks() { - api - .get(`/pipelines/${pipeline.id}`) - .reply(200, pipeline) - .get(`/pipelines/${pipeline.id}/pipeline-couplings`) - .reply(200, [coupling]) - .post('/filters/apps') - .reply(200, [app]) + function setupSDKMocks() { + const infoStub = stub().resolves(pipeline) + const listAppsStub = stub().resolves([app]) + sdkMock = mockSDKPlatform({ + pipeline: {info: infoStub}, + pipelineCoupling: {listApps: listAppsStub}, + }) } it('transfers to a team', async function () { this.retries(2) - setupCommonMocks() + setupSDKMocks() api .get(`/teams/${team.name}`) @@ -75,7 +78,7 @@ describe('pipelines:transfer', function () { }) it('transfers to an account', async function () { - setupCommonMocks() + setupSDKMocks() api .get(`/users/${account.email}`) @@ -94,7 +97,7 @@ describe('pipelines:transfer', function () { it('does not pass confirm flag', async function () { const promptStub = stub(hux, 'prompt').onFirstCall().resolves(pipeline.name) - setupCommonMocks() + setupSDKMocks() api .get(`/users/${account.email}`) diff --git a/test/unit/commands/pipelines/update.unit.test.ts b/test/unit/commands/pipelines/update.unit.test.ts index 618b866831..5ee4286f4d 100644 --- a/test/unit/commands/pipelines/update.unit.test.ts +++ b/test/unit/commands/pipelines/update.unit.test.ts @@ -1,34 +1,31 @@ import {runCommand} from '@heroku-cli/test-utils' import {expect} from 'chai' -import nock from 'nock' +import {restore, stub} from 'sinon' import PipelinesUpdate from '../../../../src/commands/pipelines/update.js' +import {type MockSDK, mockSDKPlatform} from '../../../helpers/mock-sdk.js' describe('pipelines:update', function () { const app = 'example' const id = '0123' const stage = 'production' const coupling = {id, stage} - let api: nock.Scope - - beforeEach(function () { - api = nock('https://api.heroku.com') - }) + let sdkMock: MockSDK afterEach(function () { - api.done() - nock.cleanAll() + sdkMock.restore() + restore() }) it('displays the right messages', async function () { - api - .get(`/apps/${app}/pipeline-couplings`) - .reply(200, coupling) - .patch(`/pipeline-couplings/${id}`) - .reply(200, coupling) + const infoByAppStub = stub().resolves(coupling) + const updateStub = stub().resolves(coupling) + sdkMock = mockSDKPlatform({pipelineCoupling: {infoByApp: infoByAppStub, update: updateStub}}) const {stderr} = await runCommand(PipelinesUpdate, [`--app=${app}`, `--stage=${stage}`]) expect(stderr).to.include(`Changing ⬢ ${app} to ${stage}... done`) + expect(infoByAppStub.calledOnceWith(app)).to.be.true + expect(updateStub.calledOnceWith(id, {stage})).to.be.true }) })