From be8942f61bbc33e69379775faf93ea6952a95377 Mon Sep 17 00:00:00 2001 From: Erika Wallace Date: Fri, 9 Oct 2026 17:53:17 -0400 Subject: [PATCH] refactor: use @heroku/sdk for pg:backups commands (part 3) --- V12-CHANGES.md | 1 + package-lock.json | 8 +- package.json | 2 +- src/commands/pg/backups/capture.ts | 70 ++++---- src/commands/pg/backups/restore.ts | 58 ++++--- src/lib/pg/backups.ts | 66 +++++++- .../commands/pg/backups/capture.unit.test.ts | 129 +++++++-------- .../commands/pg/backups/restore.unit.test.ts | 156 ++++++++++-------- 8 files changed, 292 insertions(+), 198 deletions(-) diff --git a/V12-CHANGES.md b/V12-CHANGES.md index bd2b891ee9..1214b2c4a7 100644 --- a/V12-CHANGES.md +++ b/V12-CHANGES.md @@ -26,6 +26,7 @@ Many commands now resolve add-ons, add-on attachments, and Postgres databases th - `pg:backups:unschedule` now names the `DATABASE` argument, or the auto-selected schedule name, in the “no daily backups found” error instead of the add-on resource name. - `pg:backups:schedule` now names the `DATABASE` argument, or `DATABASE_URL` when that argument is omitted, in the "not yet provisioned" error instead of the add-on resource name. The scheduling spinner still shows that add-on name when `database.describe` returns `name`; if `name` is absent it falls back to the `DATABASE` argument, then `DATABASE_URL`. +- `pg:backups:capture` now names the `DATABASE` argument, or `DATABASE_URL` when that argument is omitted, in the "not yet provisioned" error instead of the add-on resource name. The backup spinner still shows the resolved add-on name, since `captureAndWait` resolves it independently. ## Run commands diff --git a/package-lock.json b/package-lock.json index dd43b48a34..fcfd0a6e2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@heroku/heroku-fetch": "^0.1.1-beta", "@heroku/http-call": "^5.5.1", "@heroku/mcp-server": "^1.2.9", - "@heroku/sdk": "^0.8.1", + "@heroku/sdk": "^0.9.0", "@heroku/types": "^5.2.0", "@inquirer/prompts": "^8.7", "@oclif/core": "^4.8.4", @@ -3516,9 +3516,9 @@ } }, "node_modules/@heroku/sdk": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@heroku/sdk/-/sdk-0.8.1.tgz", - "integrity": "sha512-Ka8E4ukU5sQPbF/gcSgJ/ISUBW9dRjz+VyZ0vro+FiP4X6FuYMx55T1AJdGZuTCRivy7ZjHhP5kPa41C6RcZOQ==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@heroku/sdk/-/sdk-0.9.0.tgz", + "integrity": "sha512-QfE1T+rRdCpaxxzlc9L2MvntPegO1dBZtIrniRkm4jtFf5TEFQBZlzJSaORz1G+DjJah/9FBHDO74VCwBrZ0DA==", "license": "Apache-2.0", "dependencies": { "@heroku/heroku-fetch": "^0.1.4", diff --git a/package.json b/package.json index 569b6cc4b9..3b8cb67d14 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "@heroku/heroku-fetch": "^0.1.1-beta", "@heroku/http-call": "^5.5.1", "@heroku/mcp-server": "^1.2.9", - "@heroku/sdk": "^0.8.1", + "@heroku/sdk": "^0.9.0", "@heroku/types": "^5.2.0", "@inquirer/prompts": "^8.7", "@oclif/core": "^4.8.4", diff --git a/src/commands/pg/backups/capture.ts b/src/commands/pg/backups/capture.ts index 4686f0c93d..cae96962ca 100644 --- a/src/commands/pg/backups/capture.ts +++ b/src/commands/pg/backups/capture.ts @@ -1,11 +1,12 @@ import {Command, flags} from '@heroku-cli/command' -import {color, utils} from '@heroku/heroku-cli-util' -import {HTTPError} from '@heroku/http-call' +import {color} from '@heroku/heroku-cli-util' +import {NotFoundError} from '@heroku/heroku-fetch' +import {HerokuSDK} from '@heroku/sdk' +import {backupExtensions, databaseExtensions, TransferFailedError} from '@heroku/sdk/extensions/data' import {Args, ux} from '@oclif/core' import tsheredoc from 'tsheredoc' -import backupsApi from '../../../lib/pg/backups.js' -import {BackupTransfer, PgDatabase} from '../../../lib/pg/types.js' +import * as pgBackups from '../../../lib/pg/backups.js' import {nls} from '../../../nls.js' const heredoc = tsheredoc @@ -29,47 +30,58 @@ export default class Capture extends Command { const {database} = args const interval = Math.max(3, Number.parseInt(waitInterval || '3', 10)) - const dbResolver = new utils.pg.DatabaseResolver(this.heroku) - const {addon: db} = await dbResolver.getAttachment(app, database) - const pgBackupsApi = backupsApi(app, this.heroku) + const {data} = new HerokuSDK({extensions: [backupExtensions, databaseExtensions]}) try { - const {body: dbInfo} = await this.heroku.get(`/client/v11/databases/${db.id}`, {hostname: utils.pg.host()}) - const dbProtected = /On/.test(dbInfo.info.find(attribute => attribute.name === 'Continuous Protection')?.values[0] || '') + const dbInfo = await data.database.describe(app, database) + const dbProtected = /On/.test(String(dbInfo.info?.find(attribute => attribute.name === 'Continuous Protection')?.values?.[0])) if (dbProtected) { ux.warn('Continuous protection is already enabled for this database. Logical backups of large databases are likely to fail.') ux.warn('See https://devcenter.heroku.com/articles/heroku-postgres-data-safety-and-continuous-protection#physical-backups-on-heroku-postgres.') } - } catch (error: unknown) { - const httpError = error as HTTPError - if (httpError.statusCode !== 404) - throw httpError + } catch (error) { + if (!(error instanceof NotFoundError)) + throw error ux.error( heredoc` - ${color.datastore(db.name)} is not yet provisioned. + ${color.datastore(database ?? 'DATABASE_URL')} is not yet provisioned. Run ${color.code('heroku addons:wait')} to wait until the db is provisioned. `, {exit: 1}, ) } - ux.action.start(`Starting backup of ${color.datastore(db.name)}`) - const {body: backup} = await this.heroku.post(`/client/v11/databases/${db.id}/backups`, {hostname: utils.pg.host()}) - ux.action.stop() - ux.stdout(heredoc` + const waitOptions = pgBackups.constructWaitOptions(interval, verbose, backup => `Backing up ${backup.from_name ? color.datastore(backup.from_name) : 'UNKNOWN'} to ${color.cyan(pgBackups.name(backup))}`) - Use Ctrl-C at any time to stop monitoring progress; the backup will continue running. - Use ${color.code('heroku pg:backups:info')} to check progress. - Stop a running backup with ${color.code('heroku pg:backups:cancel')}. - `) + try { + await data.backup.captureAndWait(app, database, { + capturePoller: { + onStart(db) { + ux.action.start(`Starting backup of ${color.datastore(db.name)}`) + }, + onStop(db) { + ux.action.stop() + ux.stdout(heredoc` - if (app !== db.app.name) { - ux.stdout(heredoc` - HINT: You are running this command with a non-billing application. - Use ${color.code('heroku pg:backups -a ' + db.app.name)} to check the list of backups. - `) - } + Use Ctrl-C at any time to stop monitoring progress; the backup will continue running. + Use ${color.code('heroku pg:backups:info')} to check progress. + Stop a running backup with ${color.code('heroku pg:backups:cancel')}. + `) - await pgBackupsApi.wait(`Backing up ${color.datastore(backup.from_name)} to ${color.cyan(pgBackupsApi.name(backup))}`, backup.uuid, interval, verbose, db.app.name || app) + if (app !== db.app.name) { + ux.stdout(heredoc` + HINT: You are running this command with a non-billing application. + Use ${color.code('heroku pg:backups -a ' + db.app.name)} to check the list of backups. + `) + } + }, + }, + verbose, + ...waitOptions, + }) + } catch (error) { + if (!(error instanceof TransferFailedError)) throw error + pgBackups.reportTransferFailure(error, 'backup') + } } } diff --git a/src/commands/pg/backups/restore.ts b/src/commands/pg/backups/restore.ts index c897a72c8e..6b6908091b 100644 --- a/src/commands/pg/backups/restore.ts +++ b/src/commands/pg/backups/restore.ts @@ -1,12 +1,12 @@ import {Command, flags} from '@heroku-cli/command' -import {color, utils} from '@heroku/heroku-cli-util' +import {color} from '@heroku/heroku-cli-util' +import {HerokuSDK} from '@heroku/sdk' +import {restoreExtensions, TransferFailedError} from '@heroku/sdk/extensions/data' import {Args, ux} from '@oclif/core' import tsheredoc from 'tsheredoc' -import type {BackupTransfer} from '../../../lib/pg/types.js' - import ConfirmCommand from '../../../lib/confirm-command.js' -import backupsFactory from '../../../lib/pg/backups.js' +import * as pgBackups from '../../../lib/pg/backups.js' import {nls} from '../../../nls.js' const heredoc = tsheredoc @@ -68,9 +68,8 @@ export default class Restore extends Command { const {args, flags} = await this.parse(Restore) const {app, confirm, extensions, verbose, 'wait-interval': waitInterval} = flags const interval = Math.max(3, waitInterval) - const dbResolver = new utils.pg.DatabaseResolver(this.heroku) - const {addon: db} = await dbResolver.getAttachment(app as string, args.database) - const pgbackups = backupsFactory(app, this.heroku) + const {data} = new HerokuSDK({extensions: [restoreExtensions]}) + let backupURL let backupName = args.backup @@ -84,12 +83,12 @@ export default class Restore extends Command { backupApp = app } - const {body: transfers} = await this.heroku.get(`/client/v11/apps/${backupApp}/transfers`, {hostname: utils.pg.host()}) + const transfers = await data.transfer.listByApp(backupApp) const backups = transfers.filter(t => t.from_type === 'pg_dump' && t.to_type === 'gof3r') let backup if (backupName) { - backup = backups.find(b => pgbackups.name(b) === backupName) + backup = backups.find(b => pgBackups.name(b) === backupName) if (!backup) throw new Error(`Backup ${color.cyan(backupName)} not found for ${color.app(backupApp)}`) if (!backup.succeeded) @@ -108,7 +107,7 @@ export default class Restore extends Command { throw new Error(`No backups for ${color.app(backupApp)}. Capture one with ${color.code('heroku pg:backups:capture')}`) } - backupName = pgbackups.name(backup) + backupName = pgBackups.name(backup) } backupURL = backup.to_url @@ -116,20 +115,33 @@ export default class Restore extends Command { const confirmCmd = new ConfirmCommand() await confirmCmd.confirm(app, confirm) - ux.action.start(`Starting restore of ${color.cyan(backupName)} to ${color.datastore(db.name)}`) - ux.stdout(heredoc(` - - Use Ctrl-C at any time to stop monitoring progress; the backup will continue restoring. - Use ${color.code('heroku pg:backups')} to check progress. - Stop a running restore with ${color.code('heroku pg:backups:cancel')}. - `)) - const {body: restore} = await this.heroku.post<{uuid: string}>(`/client/v11/databases/${db.id}/restores`, { - body: {backup_url: backupURL, extensions: this.getSortedExtensions(extensions as string)}, hostname: utils.pg.host(), - }) - - ux.action.stop() - await pgbackups.wait('Restoring', restore.uuid, interval, verbose, db.app.id as string) + const waitOptions = pgBackups.constructWaitOptions(interval, verbose, () => 'Restoring') + + try { + await data.restore.restoreAndWait(app, args.database, backupURL as string, { + extensions: this.getSortedExtensions(extensions as string), + restorePoller: { + onStart(addon) { + ux.action.start(`Starting restore of ${color.cyan(backupName)} to ${color.datastore(addon.name)}`) + ux.stdout(heredoc(` + + Use Ctrl-C at any time to stop monitoring progress; the backup will continue restoring. + Use ${color.code('heroku pg:backups')} to check progress. + Stop a running restore with ${color.code('heroku pg:backups:cancel')}. + `)) + }, + onStop() { + ux.action.stop() + }, + }, + verbose, + ...waitOptions, + }) + } catch (error) { + if (!(error instanceof TransferFailedError)) throw error + pgBackups.reportTransferFailure(error, 'restore') + } } } diff --git a/src/lib/pg/backups.ts b/src/lib/pg/backups.ts index 9ef6d4e3c2..df4336d787 100644 --- a/src/lib/pg/backups.ts +++ b/src/lib/pg/backups.ts @@ -1,8 +1,14 @@ import type {HerokuSDK} from '@heroku/sdk' +import type {TransferFailedError} from '@heroku/sdk/extensions/data' +import type {CaptureAndWaitOptions, RestoreAndWaitOptions, WaitForTransferOptions} from '@heroku/sdk/extensions/data' import type {TransferInfoByAppResult} from '@heroku/types/data' +import {color, utils} from '@heroku/heroku-cli-util' import {ux} from '@oclif/core/ux' import bytes from 'bytes' +import tsheredoc from 'tsheredoc' + +const heredoc = tsheredoc type Data = HerokuSDK['data'] @@ -86,15 +92,65 @@ function prefix(transfer: TransferInfoByAppResult) { } } -// TODO: Temporary default export so pg:backups:capture, pg:backups:restore, and pg:copy still compile. Remove it when those commands move to @heroku/sdk. +type TransferWaitOptions + = Pick + & Pick + +/** + * Wait-phase options for `captureAndWait` and `restoreAndWait`. + * Spread the result into the SDK call. + */ +export function constructWaitOptions( + interval: number, + verbose: boolean | undefined, + action: (transfer: TransferInfoByAppResult) => string, +): TransferWaitOptions { + const logs = new LogDisplay() + const tty = process.env.TERM !== 'dumb' && Boolean(process.stderr.isTTY) + + return { + intervalMs: interval * 1000, + onPoll(transfer) { + if (verbose) { + logs.displayLogs(transfer?.logs) + } else if (tty) { + const msg = transfer?.started_at ? filesize(transfer.processed_bytes) : 'pending' + const log = transfer?.logs?.pop() + ux.action.status = log ? `${msg}\n${log.created_at + ' ' + log.message}` : msg + } + }, + waitPoller: { + onStart(transfer) { + const label = action(transfer) + if (verbose) { + ux.stdout(`${label}...`) + } + + ux.action.start(label) + }, + onStop: () => ux.action.stop(), + }, + } +} + +export function reportTransferFailure(error: TransferFailedError, transferType: 'backup' | 'restore'): void { + ux.action.stop('!') + + const {transfer} = error + + ux.error(new Error(heredoc(` + An error occurred and the ${transferType} did not finish. + + ${(transfer.logs || []).slice(-5).map(l => l.message).join('\n')} + + Run ${color.code('heroku pg:backups:info ' + name(transfer))} for more details.`))) +} + +// TODO: Temporary default export so pg:copy still compiles. Remove it when that command moves to @heroku/sdk. import {APIClient} from '@heroku-cli/command' -import {color, utils} from '@heroku/heroku-cli-util' -import tsheredoc from 'tsheredoc' import type {BackupTransfer} from './types.js' -const heredoc = tsheredoc - export default function backupsFactory(app: string, heroku: APIClient) { const logs = new LogDisplay() diff --git a/test/unit/commands/pg/backups/capture.unit.test.ts b/test/unit/commands/pg/backups/capture.unit.test.ts index 0e28606a27..3318181d24 100644 --- a/test/unit/commands/pg/backups/capture.unit.test.ts +++ b/test/unit/commands/pg/backups/capture.unit.test.ts @@ -1,52 +1,69 @@ +import type {CaptureAndWaitOptions} from '@heroku/sdk/extensions/data' + import {runCommand} from '@heroku-cli/test-utils' import {expect} from 'chai' -import nock from 'nock' +import {SinonStub, stub} from 'sinon' import tsheredoc from 'tsheredoc' import Cmd from '../../../../../src/commands/pg/backups/capture.js' +import {type MockSDK, mockSDKData} from '../../../../helpers/mock-sdk.js' const heredoc = tsheredoc +type FakeData = { + backup: {captureAndWait: SinonStub} + database: {describe: SinonStub} +} + +function buildFakeData(): FakeData { + return { + backup: {captureAndWait: stub()}, + database: {describe: stub()}, + } +} + +function fakeCaptureAndWait(addon: Record, transfer: Record) { + return async (_app: string, _database: string | undefined, options: CaptureAndWaitOptions = {}) => { + const database = addon as never + const info = transfer as never + + options.capturePoller?.onStart?.(database) + options.capturePoller?.onStop?.(database) + options.waitPoller?.onStart?.(info) + options.onPoll?.(info) + options.waitPoller?.onStop?.(info) + + return transfer + } +} + describe('pg:backups:capture', function () { const addon = { app: {name: 'myapp'}, id: 1, name: 'postgres-1', plan: {name: 'heroku-postgresql:standard-0'}, } - let api: nock.Scope - let pgApi: nock.Scope + let fakeData: FakeData + let sdkMock: MockSDK beforeEach(function () { - api = nock('https://api.heroku.com') - pgApi = nock('https://api.data.heroku.com') + fakeData = buildFakeData() + sdkMock = mockSDKData(fakeData) }) afterEach(function () { - nock.cleanAll() - api.done() - pgApi.done() + sdkMock.restore() }) it('captures a db', async function () { - const dbA = { + const transfer = { + finished_at: '101', from_name: 'DATABASE', num: 5, succeeded: true, uuid: '100-001', + } + + fakeData.database.describe.resolves({ info: [ {name: 'Continuous Protection', values: ['On']}, ], - } - api - .post('/actions/addon-attachments/resolve', { - addon_attachment: 'DATABASE_URL', app: 'myapp', - }) - .reply(200, [{addon}]) - pgApi - .post('/client/v11/databases/1/backups') - .reply(200, { - from_name: 'DATABASE', num: 5, uuid: '100-001', - }) - .get('/client/v11/apps/myapp/transfers/100-001') - .reply(200, { - finished_at: '101', succeeded: true, - }) - .get('/client/v11/databases/1') - .reply(200, dbA) + }) + fakeData.backup.captureAndWait.callsFake(fakeCaptureAndWait(addon, transfer)) const {stderr, stdout} = await runCommand(Cmd, [ '--app', @@ -65,30 +82,23 @@ describe('pg:backups:capture', function () { Backing up ⛁ DATABASE to b005... done `)) expect(stderr).to.match(/backups of large databases are likely to fail/) + // eslint-disable-next-line unicorn/no-useless-undefined + expect(fakeData.database.describe.calledOnceWithExactly('myapp', undefined)).to.equal(true) + expect(fakeData.backup.captureAndWait.firstCall.args[0]).to.equal('myapp') + expect(fakeData.backup.captureAndWait.firstCall.args[1]).to.equal(undefined) }) it('captures a db (verbose)', async function () { - const dbA = { + const transfer = { + finished_at: '101', from_name: 'DATABASE', logs: [{created_at: '100', message: 'log message 1'}], num: 5, succeeded: true, uuid: '100-001', + } + + fakeData.database.describe.resolves({ info: [ {name: 'Continuous Protection', values: ['Off']}, ], - } - api - .post('/actions/addon-attachments/resolve', { - addon_attachment: 'DATABASE_URL', app: 'myapp', - }) - .reply(200, [{addon}]) - pgApi - .post('/client/v11/databases/1/backups') - .reply(200, { - from_name: 'DATABASE', num: 5, uuid: '100-001', - }) - .get('/client/v11/apps/myapp/transfers/100-001?verbose=true') - .reply(200, { - finished_at: '101', logs: [{created_at: '100', message: 'log message 1'}], succeeded: true, - }) - .get('/client/v11/databases/1') - .reply(200, dbA) + }) + fakeData.backup.captureAndWait.callsFake(fakeCaptureAndWait(addon, transfer)) const {stderr, stdout} = await runCommand(Cmd, [ '--app', @@ -96,9 +106,6 @@ describe('pg:backups:capture', function () { '--verbose', ]) - api.done() - pgApi.done() - expect(stdout).to.equal(heredoc` Use Ctrl-C at any time to stop monitoring progress; the backup will continue running. @@ -114,27 +121,16 @@ describe('pg:backups:capture', function () { it('captures a db (verbose) with non billing app', async function () { addon.app.name = 'mybillingapp' - const dbA = { + const transfer = { + finished_at: '101', from_name: 'DATABASE', logs: [{created_at: '100', message: 'log message 1'}], num: 5, succeeded: true, uuid: '100-001', + } + + fakeData.database.describe.resolves({ info: [ {name: 'Continuous Protection', values: ['On']}, ], - } - api - .post('/actions/addon-attachments/resolve', { - addon_attachment: 'DATABASE_URL', app: 'myapp', - }) - .reply(200, [{addon}]) - pgApi - .post('/client/v11/databases/1/backups') - .reply(200, { - from_name: 'DATABASE', num: 5, uuid: '100-001', - }) - .get('/client/v11/apps/mybillingapp/transfers/100-001?verbose=true') - .reply(200, { - finished_at: '101', logs: [{created_at: '100', message: 'log message 1'}], succeeded: true, - }) - .get('/client/v11/databases/1') - .reply(200, dbA) + }) + fakeData.backup.captureAndWait.callsFake(fakeCaptureAndWait(addon, transfer)) const {stderr, stdout} = await runCommand(Cmd, [ '--app', @@ -142,9 +138,6 @@ describe('pg:backups:capture', function () { '--verbose', ]) - api.done() - pgApi.done() - expect(stdout).to.equal(heredoc` Use Ctrl-C at any time to stop monitoring progress; the backup will continue running. diff --git a/test/unit/commands/pg/backups/restore.unit.test.ts b/test/unit/commands/pg/backups/restore.unit.test.ts index c4842a5ed8..15fd2923a6 100644 --- a/test/unit/commands/pg/backups/restore.unit.test.ts +++ b/test/unit/commands/pg/backups/restore.unit.test.ts @@ -1,9 +1,12 @@ +import type {RestoreAndWaitOptions} from '@heroku/sdk/extensions/data' + import {runCommand} from '@heroku-cli/test-utils' import {expect} from 'chai' -import nock from 'nock' +import {SinonStub, stub} from 'sinon' import tsheredoc from 'tsheredoc' import Cmd from '../../../../../src/commands/pg/backups/restore.js' +import {type MockSDK, mockSDKData} from '../../../../helpers/mock-sdk.js' const heredoc = tsheredoc const addon = { @@ -12,40 +15,63 @@ const addon = { name: 'postgres-1', plan: {name: 'heroku-postgresql:standard-0'}, } + +type FakeData = { + restore: {restoreAndWait: SinonStub} + transfer: {listByApp: SinonStub} +} + +function buildFakeData(): FakeData { + return { + restore: {restoreAndWait: stub()}, + transfer: {listByApp: stub()}, + } +} + +function fakeRestoreAndWait(addon: Record, transfer: Record) { + return async ( + _app: string, + _database: string | undefined, + _backupUrl: string, + options: RestoreAndWaitOptions = {}, + ) => { + const database = addon as never + const info = transfer as never + + options.restorePoller?.onStart?.(database) + options.restorePoller?.onStop?.(database) + options.waitPoller?.onStart?.(info) + options.onPoll?.(info) + options.waitPoller?.onStop?.(info) + + return transfer + } +} + describe('pg:backups:restore', function () { - let pg: nock.Scope - let api: nock.Scope - - beforeEach(async function () { - api = nock('https://api.heroku.com') - api.post('/actions/addon-attachments/resolve', { - addon_attachment: 'DATABASE_URL', app: 'myapp', - }).reply(200, [{addon}]) - pg = nock('https://api.data.heroku.com') + let fakeData: FakeData + let sdkMock: MockSDK + + beforeEach(function () { + fakeData = buildFakeData() + sdkMock = mockSDKData(fakeData) }) - afterEach(async function () { - nock.cleanAll() - pg.done() - api.done() + afterEach(function () { + sdkMock.restore() }) context('b005', function () { - beforeEach(async function () { - pg.get('/client/v11/apps/myapp/transfers') - .reply(200, [ - { - from_type: 'pg_dump', num: 5, succeeded: true, to_type: 'gof3r', to_url: 'https://myurl', - }, - ]) - pg.post('/client/v11/databases/1/restores', {backup_url: 'https://myurl'}) - .reply(200, { - from_name: 'DATABASE', num: 5, uuid: '100-001', - }) - pg.get('/client/v11/apps/myapp/transfers/100-001') - .reply(200, { - finished_at: '101', succeeded: true, - }) + beforeEach(function () { + fakeData.transfer.listByApp.resolves([ + { + from_type: 'pg_dump', num: 5, succeeded: true, to_type: 'gof3r', to_url: 'https://myurl', + }, + ]) + fakeData.restore.restoreAndWait.callsFake(fakeRestoreAndWait( + addon, + {finished_at: '101', succeeded: true, uuid: '100-001'}, + )) }) it('restores a db', async function () { @@ -66,6 +92,7 @@ describe('pg:backups:restore', function () { Starting restore of b005 to ⛁ postgres-1... done Restoring... done `)) + expect(fakeData.restore.restoreAndWait.firstCall.args.slice(0, 3)).to.deep.equal(['myapp', undefined, 'https://myurl']) }) it('restores a specific db', async function () { @@ -87,6 +114,7 @@ describe('pg:backups:restore', function () { Starting restore of b005 to ⛁ postgres-1... done Restoring... done `)) + expect(fakeData.restore.restoreAndWait.firstCall.args.slice(0, 3)).to.deep.equal(['myapp', undefined, 'https://myurl']) }) it('restores a specific app db', async function () { @@ -108,25 +136,23 @@ describe('pg:backups:restore', function () { Starting restore of b005 to ⛁ postgres-1... done Restoring... done `)) + expect(fakeData.restore.restoreAndWait.firstCall.args.slice(0, 3)).to.deep.equal(['myapp', undefined, 'https://myurl']) }) }) context('b005 (verbose)', function () { - beforeEach(async function () { - pg.get('/client/v11/apps/myapp/transfers') - .reply(200, [ - { - from_type: 'pg_dump', num: 5, succeeded: true, to_type: 'gof3r', to_url: 'https://myurl', - }, - ]) - pg.post('/client/v11/databases/1/restores', {backup_url: 'https://myurl'}) - .reply(200, { - from_name: 'DATABASE', num: 5, uuid: '100-001', - }) - pg.get('/client/v11/apps/myapp/transfers/100-001?verbose=true') - .reply(200, { - finished_at: '101', logs: [{created_at: '100', message: 'log message 1'}], succeeded: true, - }) + beforeEach(function () { + fakeData.transfer.listByApp.resolves([ + { + from_type: 'pg_dump', num: 5, succeeded: true, to_type: 'gof3r', to_url: 'https://myurl', + }, + ]) + fakeData.restore.restoreAndWait.callsFake(fakeRestoreAndWait( + addon, + { + finished_at: '101', logs: [{created_at: '100', message: 'log message 1'}], succeeded: true, uuid: '100-001', + }, + )) }) it('shows verbose output', async function () { @@ -155,15 +181,11 @@ describe('pg:backups:restore', function () { }) context('with a URL', function () { - beforeEach(async function () { - pg.post('/client/v11/databases/1/restores', {backup_url: 'https://www.dropbox.com?dl=1'}) - .reply(200, { - from_name: 'DATABASE', num: 5, uuid: '100-001', - }) - pg.get('/client/v11/apps/myapp/transfers/100-001') - .reply(200, { - finished_at: '101', succeeded: true, - }) + beforeEach(function () { + fakeData.restore.restoreAndWait.callsFake(fakeRestoreAndWait( + addon, + {finished_at: '101', succeeded: true, uuid: '100-001'}, + )) }) it('restores a db from a URL', async function () { @@ -186,25 +208,22 @@ describe('pg:backups:restore', function () { Starting restore of https://www.dropbox.com to ⛁ postgres-1... done Restoring... done `)) + expect(fakeData.transfer.listByApp.called).to.equal(false) + expect(fakeData.restore.restoreAndWait.firstCall.args.slice(0, 3)).to.deep.equal(['myapp', undefined, 'https://www.dropbox.com?dl=1']) }) }) context('with extensions', function () { - beforeEach(async function () { - pg.get('/client/v11/apps/myapp/transfers') - .reply(200, [ - { - from_type: 'pg_dump', num: 5, succeeded: true, to_type: 'gof3r', to_url: 'https://myurl', - }, - ]) - pg.post('/client/v11/databases/1/restores', {backup_url: 'https://myurl', extensions: ['postgis', 'uuid-ossp']}) - .reply(200, { - from_name: 'DATABASE', num: 5, uuid: '100-001', - }) - pg.get('/client/v11/apps/myapp/transfers/100-001') - .reply(200, { - finished_at: '101', succeeded: true, - }) + beforeEach(function () { + fakeData.transfer.listByApp.resolves([ + { + from_type: 'pg_dump', num: 5, succeeded: true, to_type: 'gof3r', to_url: 'https://myurl', + }, + ]) + fakeData.restore.restoreAndWait.callsFake(fakeRestoreAndWait( + addon, + {finished_at: '101', succeeded: true, uuid: '100-001'}, + )) }) it('restores a db with pre-installed extensions', async function () { @@ -228,6 +247,7 @@ describe('pg:backups:restore', function () { Starting restore of b005 to ⛁ postgres-1... done Restoring... done `)) + expect(fakeData.restore.restoreAndWait.firstCall.args[3]).to.deep.include({extensions: ['postgis', 'uuid-ossp']}) }) }) })