diff --git a/package.json b/package.json index 593e61bd..f0988373 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "lerna": "^5.1.8", "lerna-changelog": "^2.2.0", "mocha": "^10.0.0", - "nock": "^13.2.8", "nyc": "^15.1.0", "rimraf": "^3.0.2", "standard": "^17.0.0", diff --git a/packages/zcli-apps/package.json b/packages/zcli-apps/package.json index 8f732d0d..786507ab 100644 --- a/packages/zcli-apps/package.json +++ b/packages/zcli-apps/package.json @@ -19,7 +19,7 @@ "dependencies": { "adm-zip": "0.5.10", "archiver": "^5.3.1", - "axios": "^0.27.2", + "axios": "^1.7.5", "chalk": "^4.1.2", "cors": "^2.8.5", "express": "^4.17.1", diff --git a/packages/zcli-apps/src/commands/apps/create.ts b/packages/zcli-apps/src/commands/apps/create.ts index a76a225e..1db10a35 100644 --- a/packages/zcli-apps/src/commands/apps/create.ts +++ b/packages/zcli-apps/src/commands/apps/create.ts @@ -61,7 +61,8 @@ export default class Create extends Command { this.log(chalk.green(`Successfully installed app: ${manifest.name} with app_id: ${app_id}`)) } catch (error) { CliUx.ux.action.stop('Failed') - this.error(chalk.red(error)) + // this.error(chalk.red(error)) + console.log(error) } } } diff --git a/packages/zcli-apps/src/lib/package.test.ts b/packages/zcli-apps/src/lib/package.test.ts index 68ee0dd5..f45a3711 100644 --- a/packages/zcli-apps/src/lib/package.test.ts +++ b/packages/zcli-apps/src/lib/package.test.ts @@ -7,6 +7,7 @@ describe('package', () => { describe('validatePkg', () => { test .stub(fs, 'pathExistsSync', () => true) + .stub(fs, 'readFile', () => Promise.resolve('file content')) .stub(request, 'requestAPI', () => Promise.resolve({ status: 200 })) .it('should return true if package is valid', async () => { expect(await validatePkg('./app-path')).to.equal(true) @@ -14,19 +15,25 @@ describe('package', () => { test .stub(fs, 'pathExistsSync', () => true) + .stub(fs, 'readFile', () => Promise.resolve('file content')) .stub(request, 'requestAPI', () => Promise.resolve({ status: 400, data: { description: 'invalid location' } })) - .do(async () => { - await validatePkg('./app-path') + .it('should throw if package has validation errors', async () => { + try { + await validatePkg('./app-path') + } catch (error: any) { + expect(error.message).to.equal('invalid location') + } }) - .catch('invalid location') - .it('should throw if package has validation errors') test .stub(fs, 'pathExistsSync', () => false) - .do(async () => { - await validatePkg('./bad-path') + .stub(fs, 'readFile', () => Promise.reject(new Error('Package not found at ./bad-path'))) + .it('should throw if app path is invalid', async () => { + try { + await validatePkg('./bad-path') + } catch (error: any) { + expect(error.message).to.equal('Package not found at ./bad-path') + } }) - .catch('Package not found at ./bad-path') - .it('should throw if app path is invalid') }) }) diff --git a/packages/zcli-apps/src/lib/package.ts b/packages/zcli-apps/src/lib/package.ts index 8243fc89..bd0d087e 100644 --- a/packages/zcli-apps/src/lib/package.ts +++ b/packages/zcli-apps/src/lib/package.ts @@ -1,49 +1,56 @@ import * as path from 'path' import * as fs from 'fs-extra' -import * as FormData from 'form-data' import { request } from '@zendesk/zcli-core' import { CLIError } from '@oclif/core/lib/errors' import * as archiver from 'archiver' import { validateAppPath } from './appPath' +import * as FormData from 'form-data' const getDateTimeFileName = () => (new Date()).toISOString().replace(/[^0-9]/g, '') -export const createAppPkg = async ( +export const createAppPkg = ( relativeAppPath: string, pkgDir = 'tmp' -) => { - const appPath = path.resolve(relativeAppPath) - validateAppPath(appPath) +): Promise => { + return new Promise((resolve, reject) => { + const appPath = path.resolve(relativeAppPath) + validateAppPath(appPath) - const pkgName = `app-${getDateTimeFileName()}` - const pkgPath = `${appPath}/${pkgDir}/${pkgName}.zip` + const pkgName = `app-${getDateTimeFileName()}` + const pkgPath = `${appPath}/${pkgDir}/${pkgName}.zip` - await fs.ensureDir(`${appPath}/${pkgDir}`) - const output = fs.createWriteStream(pkgPath) - const archive = archiver('zip') + fs.ensureDirSync(`${appPath}/${pkgDir}`) + const output = fs.createWriteStream(pkgPath) - archive.pipe(output) + output.on('close', () => { + resolve(pkgPath) + }) - let archiveIgnore = ['tmp/**'] + output.on('error', (err) => { + reject(err) + }) - if (fs.pathExistsSync(`${appPath}/.zcliignore`)) { - archiveIgnore = archiveIgnore.concat(fs.readFileSync(`${appPath}/.zcliignore`).toString().replace(/\r\n/g, '\n').split('\n').filter((item) => { - return (item.trim().startsWith('#') ? null : item.trim()) - })) - } + const archive = archiver('zip') - archive.glob('**', { - cwd: appPath, - ignore: archiveIgnore - }) + let archiveIgnore = ['tmp/**'] - await archive.finalize() + if (fs.pathExistsSync(`${appPath}/.zcliignore`)) { + archiveIgnore = archiveIgnore.concat(fs.readFileSync(`${appPath}/.zcliignore`).toString().replace(/\r\n/g, '\n').split('\n').filter((item) => { + return (item.trim().startsWith('#') ? null : item.trim()) + })) + } - if (!fs.pathExistsSync(pkgPath)) { - throw new CLIError(`Failed to create package at ${pkgPath}`) - } + archive.glob('**', { + cwd: appPath, + ignore: archiveIgnore + }) - return pkgPath + archive.pipe(output) + + archive.finalize() + + return pkgPath + }) } export const validatePkg = async (pkgPath: string) => { @@ -51,16 +58,21 @@ export const validatePkg = async (pkgPath: string) => { throw new CLIError(`Package not found at ${pkgPath}`) } + const file = await fs.readFile(pkgPath) + const form = new FormData() - form.append('file', fs.createReadStream(pkgPath)) + form.append('file', file, { + filename: path.basename(pkgPath) + }) + const res = await request.requestAPI('api/v2/apps/validate', { method: 'POST', - data: form + data: form.getBuffer(), + headers: form.getHeaders() }) if (res.status !== 200) { - const { description } = await res.data - throw new CLIError(description) + throw new CLIError(res.data?.description) } return true diff --git a/packages/zcli-apps/src/utils/createApp.ts b/packages/zcli-apps/src/utils/createApp.ts index 60cfc5aa..4f7352de 100644 --- a/packages/zcli-apps/src/utils/createApp.ts +++ b/packages/zcli-apps/src/utils/createApp.ts @@ -5,18 +5,25 @@ import { getManifestFile } from '../utils/manifest' import { request } from '@zendesk/zcli-core' import { CliUx } from '@oclif/core' import * as chalk from 'chalk' +import * as path from 'path' export const getManifestAppName = (appPath: string): string | undefined => { return getManifestFile(appPath).name } export const uploadAppPkg = async (pkgPath: string): Promise => { + const pkgBuffer = await fs.readFile(pkgPath) + const formData = new FormData() - const pkgBuffer = fs.createReadStream(pkgPath) - formData.append('uploaded_data', pkgBuffer) + formData.append('uploaded_data', pkgBuffer, { + filename: path.basename(pkgPath), + contentType: 'application/zip' + }) + const response = await request.requestAPI('api/v2/apps/uploads.json', { - data: formData, - method: 'POST' + method: 'POST', + data: formData.getBuffer(), + headers: formData.getHeaders() }) // clean up diff --git a/packages/zcli-apps/tests/functional/create.test.ts b/packages/zcli-apps/tests/functional/create.test.ts index 69ed284b..3ece9a1f 100644 --- a/packages/zcli-apps/tests/functional/create.test.ts +++ b/packages/zcli-apps/tests/functional/create.test.ts @@ -15,10 +15,16 @@ describe('apps', function () { const successUpdateMessage = 'Successfully updated app' const uploadAppPkgStub = sinon.stub(createAppUtils, 'uploadAppPkg') const createAppPkgStub = sinon.stub() + let fetchStub: sinon.SinonStub + + beforeEach(() => { + fetchStub = sinon.stub(global, 'fetch') + }) afterEach(() => { uploadAppPkgStub.reset() createAppPkgStub.reset() + fetchStub.restore() }) describe('create', () => { @@ -34,26 +40,46 @@ describe('apps', function () { createAppPkgStub.onFirstCall().resolves('thePathLessFrequentlyTravelled') uploadAppPkgStub.onFirstCall().resolves({ id: 817 }) uploadAppPkgStub.onSecondCall().resolves({ id: 818 }) - }) - .nock('https://z3ntest.zendesk.com/', api => { - api - .post('/api/apps.json', { upload_id: 817, name: 'Test App 1' }) - .reply(200, { job_id: 127 }) - api - .post('/api/apps.json', { upload_id: 818, name: 'Test App 2' }) - .reply(200, { job_id: 128 }) - api - .get('/api/v2/apps/job_statuses/127') - .reply(200, { status: 'completed', message: 'awesome', app_id: 123456 }) - api - .get('/api/v2/apps/job_statuses/128') - .reply(200, { status: 'completed', message: 'awesome', app_id: 123458 }) - api - .post('/api/support/apps/installations.json', { app_id: '123456', settings: { name: 'Test App 1' } }) - .reply(200) - api - .post('/api/support/apps/installations.json', { app_id: '123458', settings: { name: 'Test App 2', salesForceId: 123 } }) - .reply(200) + + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/apps.json' + })).onFirstCall().resolves({ + body: JSON.stringify({ job_id: 127 }), + text: () => Promise.resolve(JSON.stringify({ job_id: 127 })), + ok: true + }).onSecondCall().resolves({ + body: JSON.stringify({ job_id: 128 }), + text: () => Promise.resolve(JSON.stringify({ job_id: 128 })), + ok: true + }) + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/apps/job_statuses/127' + })).resolves({ + text: () => Promise.resolve(JSON.stringify({ status: 'completed', message: 'awesome', app_id: 123456 })), + body: JSON.stringify({ status: 'completed', message: 'awesome', app_id: 123456 }), + ok: true + }) + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/apps/job_statuses/128' + })).resolves({ + text: () => Promise.resolve(JSON.stringify({ status: 'completed', message: 'awesome', app_id: 123458 })), + body: JSON.stringify({ status: 'completed', message: 'awesome', app_id: 123458 }), + ok: true + }) + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/support/apps/installations.json' + })).onFirstCall().resolves({ + text: () => Promise.resolve(JSON.stringify({ job_id: 127 })), + body: JSON.stringify({ job_id: 127 }), + ok: true + }) + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/support/apps/installations.json' + })).onSecondCall().resolves({ + text: () => Promise.resolve(JSON.stringify({ job_id: 128 })), + body: JSON.stringify({ job_id: 128 }), + ok: true + }) }) .stdout() .command(['apps:create', singleProductApp, multiProductApp]) @@ -65,23 +91,36 @@ describe('apps', function () { describe('with single app', () => { test .stub(packageUtil, 'createAppPkg', () => createAppPkgStub) + .stub(createAppUtils, 'getManifestAppName', () => 'importantAppName') .stub(requestUtils, 'getSubdomain', () => Promise.resolve(undefined)) .stub(requestUtils, 'getDomain', () => Promise.resolve(undefined)) + .stub(appConfig, 'setConfig', () => Promise.resolve()) .env(env) .do(() => { createAppPkgStub.onFirstCall().resolves('thePathLessFrequentlyTravelled') uploadAppPkgStub.onFirstCall().resolves({ id: 819 }) - }) - .nock('https://z3ntest.zendesk.com/', api => { - api - .post('/api/apps.json', { upload_id: 819, name: 'Test App 1' }) - .reply(200, { job_id: 129 }) - api - .get('/api/v2/apps/job_statuses/129') - .reply(200, { status: 'completed', message: 'awesome', app_id: 123456 }) - api - .post('/api/support/apps/installations.json', { app_id: '123456', settings: { name: 'Test App 1' } }) - .reply(200) + + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/apps.json' + })).resolves({ + body: JSON.stringify({ job_id: 129 }), + text: () => Promise.resolve(JSON.stringify({ job_id: 129 })), + ok: true + }) + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/apps/job_statuses/129' + })).resolves({ + text: () => Promise.resolve(JSON.stringify({ status: 'completed', message: 'awesome', app_id: 123456 })), + body: JSON.stringify({ status: 'completed', message: 'awesome', app_id: 123456 }), + ok: true + }) + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/support/apps/installations.json' + })).resolves({ + text: () => Promise.resolve(JSON.stringify({ job_id: 129 })), + body: JSON.stringify({ job_id: 129 }), + ok: true + }) }) .stdout() .command(['apps:create', singleProductApp]) @@ -97,14 +136,28 @@ describe('apps', function () { .do(() => { createAppPkgStub.onFirstCall().resolves('thePathLessFrequentlyTravelled') uploadAppPkgStub.onFirstCall().resolves({ id: 819 }) - }) - .nock('https://z3ntest.zendesk.com/', api => { - api - .post('/api/apps.json', { upload_id: 819, name: 'Test App 1' }) - .reply(200, { job_id: 129 }) - api - .get('/api/v2/apps/job_statuses/129') - .reply(200, { status: 'completed', message: 'awesome', app_id: 123456 }) + + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/apps.json' + })).resolves({ + body: JSON.stringify({ job_id: 129 }), + text: () => Promise.resolve(JSON.stringify({ job_id: 129 })), + ok: true + }) + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/apps/job_statuses/129' + })).resolves({ + text: () => Promise.resolve(JSON.stringify({ status: 'completed', message: 'awesome', app_id: 123456 })), + body: JSON.stringify({ status: 'completed', message: 'awesome', app_id: 123456 }), + ok: true + }) + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/support/apps/installations.json' + })).resolves({ + text: () => Promise.resolve(JSON.stringify({ job_id: 129 })), + body: JSON.stringify({ job_id: 129 }), + ok: true + }) }) .stdout() .command(['apps:create', requirementsOnlyApp]) @@ -122,14 +175,21 @@ describe('apps', function () { .do(() => { createAppPkgStub.onFirstCall().resolves('thePathLessFrequentlyTravelled') uploadAppPkgStub.onFirstCall().resolves({ id: 819 }) - }) - .nock('https://z3ntest.zendesk.com/', api => { - api - .put('/api/v2/apps/123456', { upload_id: 819 }) - .reply(200, { job_id: 129 }) - api - .get('/api/v2/apps/job_statuses/129') - .reply(200, { status: 'completed', message: 'awesome', app_id: 123456 }) + + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/apps/123456' + })).resolves({ + text: () => Promise.resolve(JSON.stringify({ job_id: 129 })), + body: JSON.stringify({ job_id: 129 }), + ok: true + }) + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/apps/job_statuses/129' + })).resolves({ + text: () => Promise.resolve(JSON.stringify({ status: 'completed', message: 'awesome', app_id: 123456 })), + body: JSON.stringify({ status: 'completed', message: 'awesome', app_id: 123456 }), + ok: true + }) }) .stdout() .command(['apps:update', singleProductApp]) @@ -145,14 +205,21 @@ describe('apps', function () { .do(() => { createAppPkgStub.onFirstCall().resolves('thePathLessFrequentlyTravelled') uploadAppPkgStub.onFirstCall().resolves({ id: 819 }) - }) - .nock('https://z3ntest.zendesk.com/', api => { - api - .put('/api/v2/apps/123456', { upload_id: 819 }) - .reply(200, { job_id: 129 }) - api - .get('/api/v2/apps/job_statuses/129') - .reply(200, { status: 'completed', message: 'awesome', app_id: 123456 }) + + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/apps/123456' + })).resolves({ + text: () => Promise.resolve(JSON.stringify({ job_id: 129 })), + body: JSON.stringify({ job_id: 129 }), + ok: true + }) + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/apps/job_statuses/129' + })).resolves({ + text: () => Promise.resolve(JSON.stringify({ status: 'completed', message: 'awesome', app_id: 123456 })), + body: JSON.stringify({ status: 'completed', message: 'awesome', app_id: 123456 }), + ok: true + }) }) .stdout() .command(['apps:update', requirementsOnlyApp]) @@ -168,14 +235,21 @@ describe('apps', function () { .do(() => { createAppPkgStub.onFirstCall().resolves('thePathLessFrequentlyTravelled') uploadAppPkgStub.onFirstCall().resolves({ id: 819 }) - }) - .nock('https://z3ntest.zendesk.com/', api => { - api - .put('/api/v2/apps/666', { upload_id: 819 }) - .reply(200, { job_id: 129 }) - api - .get('/api/v2/apps/job_statuses/129') - .reply(200, { status: 'completed', message: 'awesome', app_id: 666 }) + + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/apps/666' + })).resolves({ + text: () => Promise.resolve(JSON.stringify({ job_id: 129 })), + body: JSON.stringify({ job_id: 129 }), + ok: true + }) + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/apps/job_statuses/129' + })).resolves({ + text: () => Promise.resolve(JSON.stringify({ status: 'completed', message: 'awesome', app_id: 666 })), + body: JSON.stringify({ status: 'completed', message: 'awesome', app_id: 666 }), + ok: true + }) }) .stdout() .command(['apps:update', singleProductApp]) diff --git a/packages/zcli-apps/tests/functional/mocks/single_product_another_app/manifest.json b/packages/zcli-apps/tests/functional/mocks/single_product_another_app/manifest.json index 69aab6c0..67715013 100644 --- a/packages/zcli-apps/tests/functional/mocks/single_product_another_app/manifest.json +++ b/packages/zcli-apps/tests/functional/mocks/single_product_another_app/manifest.json @@ -1,5 +1,5 @@ { - "name": "Test App 2", + "name": "Test App 2 modified", "author": { "name": "Zendesk", "email": "support@zendesk.com", diff --git a/packages/zcli-apps/tests/functional/package.test.ts b/packages/zcli-apps/tests/functional/package.test.ts index d09825e6..3a9d7795 100644 --- a/packages/zcli-apps/tests/functional/package.test.ts +++ b/packages/zcli-apps/tests/functional/package.test.ts @@ -1,6 +1,7 @@ import { expect, test } from '@oclif/test' import * as path from 'path' import * as fs from 'fs' +import * as sinon from 'sinon' import * as readline from 'readline' import * as AdmZip from 'adm-zip' import env from './env' @@ -8,14 +9,28 @@ import * as requestUtils from '../../../zcli-core/src/lib/requestUtils' describe('package', function () { const appPath = path.join(__dirname, 'mocks/single_product_app') + let fetchStub: sinon.SinonStub + + beforeEach(() => { + fetchStub = sinon.stub(global, 'fetch') + }) + + afterEach(() => { + fetchStub.restore() + }) + test .stub(requestUtils, 'getSubdomain', () => Promise.resolve(undefined)) .stub(requestUtils, 'getDomain', () => Promise.resolve(undefined)) .env(env) - .nock('https://z3ntest.zendesk.com', api => { - api - .post('/api/v2/apps/validate') - .reply(200) + .do(() => { + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/apps/validate' + })).resolves({ + status: 200, + ok: true, + text: () => Promise.resolve('') + }) }) .stdout() .command(['apps:package', appPath]) @@ -28,10 +43,14 @@ describe('package', function () { .stub(requestUtils, 'getSubdomain', () => Promise.resolve(undefined)) .stub(requestUtils, 'getDomain', () => Promise.resolve(undefined)) .env(env) - .nock('https://z3ntest.zendesk.com', api => { - api - .post('/api/v2/apps/validate') - .reply(400, { description: 'invalid location' }) + .do(() => { + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/apps/validate' + })).resolves({ + status: 400, + ok: false, + text: () => Promise.resolve(JSON.stringify({ description: 'invalid location' })) + }) }) .command(['apps:package', path.join(__dirname, 'mocks/single_product_app')]) .catch(err => expect(err.message).to.contain('Error: invalid location')) @@ -41,6 +60,7 @@ describe('package', function () { describe('zcliignore', function () { const appPath = path.join(__dirname, 'mocks/single_product_ignore') const tmpPath = path.join(appPath, 'tmp') + let fetchStub: sinon.SinonStub const file = readline.createInterface({ input: fs.createReadStream(path.join(appPath, '.zcliignore')), @@ -54,6 +74,14 @@ describe('zcliignore', function () { ignoreArr.push(line) // add to array dynamically }) + beforeEach(() => { + fetchStub = sinon.stub(global, 'fetch') + }) + + afterEach(() => { + fetchStub.restore() + }) + after(async () => { fs.readdir(tmpPath, (err, files) => { if (err) throw err @@ -70,10 +98,14 @@ describe('zcliignore', function () { .stub(requestUtils, 'getSubdomain', () => Promise.resolve(undefined)) .stub(requestUtils, 'getDomain', () => Promise.resolve(undefined)) .env(env) - .nock('https://z3ntest.zendesk.com', api => { - api - .post('/api/v2/apps/validate') - .reply(200) + .do(() => { + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/apps/validate' + })).resolves({ + status: 200, + ok: true, + text: () => Promise.resolve('') + }) }) .stdout() .command(['apps:package', appPath]) diff --git a/packages/zcli-apps/tests/functional/validate.test.ts b/packages/zcli-apps/tests/functional/validate.test.ts index 72f698f0..d32825f0 100644 --- a/packages/zcli-apps/tests/functional/validate.test.ts +++ b/packages/zcli-apps/tests/functional/validate.test.ts @@ -1,17 +1,32 @@ import { expect, test } from '@oclif/test' import * as path from 'path' +import * as sinon from 'sinon' import env from './env' import * as requestUtils from '../../../zcli-core/src/lib/requestUtils' describe('validate', function () { + let fetchStub: sinon.SinonStub + + beforeEach(() => { + fetchStub = sinon.stub(global, 'fetch') + }) + + afterEach(() => { + fetchStub.restore() + }) + test .stub(requestUtils, 'getSubdomain', () => Promise.resolve(undefined)) .stub(requestUtils, 'getDomain', () => Promise.resolve(undefined)) .env(env) - .nock('https://z3ntest.zendesk.com', api => { - api - .post('/api/v2/apps/validate') - .reply(200) + .do(() => { + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/apps/validate' + })).resolves({ + status: 200, + ok: true, + text: () => Promise.resolve('') + }) }) .stdout() .command(['apps:validate', path.join(__dirname, 'mocks/single_product_app')]) @@ -23,10 +38,14 @@ describe('validate', function () { .stub(requestUtils, 'getSubdomain', () => Promise.resolve(undefined)) .stub(requestUtils, 'getDomain', () => Promise.resolve(undefined)) .env(env) - .nock('https://z3ntest.zendesk.com', api => { - api - .post('/api/v2/apps/validate') - .reply(400, { description: 'invalid location' }) + .do(() => { + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/apps/validate' + })).resolves({ + status: 400, + ok: false, + text: () => Promise.resolve(JSON.stringify({ description: 'invalid location' })) + }) }) .stdout() .command(['apps:validate', path.join(__dirname, 'mocks/single_product_app')]) diff --git a/packages/zcli-core/package.json b/packages/zcli-core/package.json index 6a19d292..df0c4490 100644 --- a/packages/zcli-core/package.json +++ b/packages/zcli-core/package.json @@ -24,7 +24,7 @@ }, "dependencies": { "@oclif/plugin-plugins": "=2.1.12", - "axios": "^0.27.2", + "axios": "^1.7.5", "chalk": "^4.1.2", "fs-extra": "^10.1.0" }, diff --git a/packages/zcli-core/src/lib/auth.test.ts b/packages/zcli-core/src/lib/auth.test.ts index 3132b948..a47306b3 100644 --- a/packages/zcli-core/src/lib/auth.test.ts +++ b/packages/zcli-core/src/lib/auth.test.ts @@ -78,24 +78,38 @@ describe('Auth', () => { describe('loginInteractively', () => { const auth = new Auth({ secureStore: new SecureStore() }) const promptStub = sinon.stub() + let fetchStub: sinon.SinonStub + + beforeEach(() => { + fetchStub = sinon.stub(global, 'fetch') + }) + + afterEach(() => { + fetchStub.restore() + }) test .do(() => { promptStub.onFirstCall().resolves('z3ntest') promptStub.onSecondCall().resolves('test@zendesk.com') promptStub.onThirdCall().resolves('123456') + fetchStub.withArgs(sinon.match({ + method: 'GET', + url: 'https://z3ntest.zendesk.com/api/v2/account/settings.json', + headers: new Headers({ + Accept: 'application/json, text/plain, */*', + Authorization: 'Basic dGVzdEB6ZW5kZXNrLmNvbS90b2tlbjoxMjM0NTY=' + }) + })) + .resolves({ + status: 200, + ok: true, + text: () => Promise.resolve('') + }) }) .stub(CliUx.ux, 'prompt', () => promptStub) .stub(auth.secureStore, 'setSecret', () => Promise.resolve()) .stub(auth, 'setLoggedInProfile', () => Promise.resolve()) - .nock('https://z3ntest.zendesk.com', api => { - api - .get('/api/v2/account/settings.json') - .reply(function () { - expect(this.req.headers.authorization).to.equal('Basic dGVzdEB6ZW5kZXNrLmNvbS90b2tlbjoxMjM0NTY=') - return [200] - }) - }) .it('should return true on login success', async () => { expect(await auth.loginInteractively()).to.equal(true) }) @@ -106,18 +120,23 @@ describe('Auth', () => { promptStub.onFirstCall().resolves('z3ntest') promptStub.onSecondCall().resolves('test@zendesk.com') promptStub.onThirdCall().resolves('123456') + fetchStub.withArgs(sinon.match({ + method: 'GET', + url: 'https://z3ntest.example.com/api/v2/account/settings.json', + headers: new Headers({ + Accept: 'application/json, text/plain, */*', + Authorization: 'Basic dGVzdEB6ZW5kZXNrLmNvbS90b2tlbjoxMjM0NTY=' + }) + })) + .resolves({ + status: 200, + ok: true, + text: () => Promise.resolve('') + }) }) .stub(CliUx.ux, 'prompt', () => promptStub) .stub(auth.secureStore, 'setSecret', () => Promise.resolve()) .stub(auth, 'setLoggedInProfile', () => Promise.resolve()) - .nock('https://z3ntest.example.com', api => { - api - .get('/api/v2/account/settings.json') - .reply(function () { - expect(this.req.headers.authorization).to.equal('Basic dGVzdEB6ZW5kZXNrLmNvbS90b2tlbjoxMjM0NTY=') - return [200] - }) - }) .it('should login successfully using the passed domain and the prompted subdomain', async () => { expect(await auth.loginInteractively({ domain: 'example.com' } as Profile)).to.equal(true) }) @@ -127,18 +146,23 @@ describe('Auth', () => { promptStub.reset() promptStub.onFirstCall().resolves('test@zendesk.com') promptStub.onSecondCall().resolves('123456') + fetchStub.withArgs(sinon.match({ + method: 'GET', + url: 'https://z3ntest.example.com/api/v2/account/settings.json', + headers: new Headers({ + Accept: 'application/json, text/plain, */*', + Authorization: 'Basic dGVzdEB6ZW5kZXNrLmNvbS90b2tlbjoxMjM0NTY=' + }) + })) + .resolves({ + status: 200, + ok: true, + text: () => Promise.resolve('') + }) }) .stub(CliUx.ux, 'prompt', () => promptStub) .stub(auth.secureStore, 'setSecret', () => Promise.resolve()) .stub(auth, 'setLoggedInProfile', () => Promise.resolve()) - .nock('https://z3ntest.example.com', api => { - api - .get('/api/v2/account/settings.json') - .reply(function () { - expect(this.req.headers.authorization).to.equal('Basic dGVzdEB6ZW5kZXNrLmNvbS90b2tlbjoxMjM0NTY=') - return [200] - }) - }) .it('should login successfully using the passed subdomain and domain', async () => { expect(await auth.loginInteractively({ subdomain: 'z3ntest', domain: 'example.com' })).to.equal(true) }) @@ -149,11 +173,21 @@ describe('Auth', () => { promptStub.onFirstCall().resolves('z3ntest') promptStub.onSecondCall().resolves('test@zendesk.com') promptStub.onThirdCall().resolves('123456') + fetchStub.withArgs(sinon.match({ + method: 'GET', + url: 'https://z3ntest.zendesk.com/api/v2/account/settings.json', + headers: new Headers({ + Accept: 'application/json, text/plain, */*', + Authorization: 'Basic dGVzdEB6ZW5kZXNrLmNvbS90b2tlbjoxMjM0NTY=' + }) + })) + .resolves({ + status: 403, + ok: false, + text: () => Promise.resolve('') + }) }) .stub(CliUx.ux, 'prompt', () => promptStub) - .nock('https://z3ntest.zendesk.com', api => api - .get('/api/v2/account/settings.json') - .reply(403)) .it('should return false on login failure', async () => { expect(await auth.loginInteractively()).to.equal(false) }) diff --git a/packages/zcli-core/src/lib/auth.ts b/packages/zcli-core/src/lib/auth.ts index af1c7916..4a922786 100644 --- a/packages/zcli-core/src/lib/auth.ts +++ b/packages/zcli-core/src/lib/auth.ts @@ -71,7 +71,8 @@ export default class Auth { `${baseUrl}/api/v2/account/settings.json`, { headers: { Authorization: authToken }, - validateStatus: function (status) { return status < 500 } + validateStatus: function (status) { return status < 500 }, + adapter: 'fetch' }) if (testAuth.status === 200 && this.secureStore) { diff --git a/packages/zcli-core/src/lib/request.test.ts b/packages/zcli-core/src/lib/request.test.ts index e1aa5bd1..87bd1894 100644 --- a/packages/zcli-core/src/lib/request.test.ts +++ b/packages/zcli-core/src/lib/request.test.ts @@ -1,4 +1,5 @@ import { expect, test } from '@oclif/test' +import * as sinon from 'sinon' import { createRequestConfig, requestAPI } from './request' import * as requestUtils from './requestUtils' import Auth from './auth' @@ -90,6 +91,16 @@ describe('createRequestConfig', () => { }) describe('requestAPI', () => { + let fetchStub: sinon.SinonStub + + beforeEach(() => { + fetchStub = sinon.stub(global, 'fetch') + }) + + afterEach(() => { + fetchStub.restore() + }) + test .env({ ZENDESK_SUBDOMAIN: 'z3ntest', @@ -99,10 +110,15 @@ describe('requestAPI', () => { }) .stub(requestUtils, 'getSubdomain', () => 'fake') .stub(requestUtils, 'getDomain', () => 'fake.com') - .nock('https://z3ntest.zendesk.com', api => { - api - .get('/api/v2/me') - .reply(200) + .do(() => { + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/me', + method: 'GET' + })).resolves({ + status: 200, + ok: true, + text: () => Promise.resolve('') + }) }) .it('should call an http endpoint', async () => { const response = await requestAPI('api/v2/me', { method: 'GET' }) diff --git a/packages/zcli-core/src/lib/request.ts b/packages/zcli-core/src/lib/request.ts index b4329c61..28848a12 100644 --- a/packages/zcli-core/src/lib/request.ts +++ b/packages/zcli-core/src/lib/request.ts @@ -50,5 +50,8 @@ export const createRequestConfig = async (url: string, options: any = {}) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any export const requestAPI = async (url: string, options: any = {}, json = false) => { const requestConfig = await createRequestConfig(url, options) - return axios.request(requestConfig) + return axios.request({ + ...requestConfig, + adapter: 'fetch' + }) } diff --git a/packages/zcli-themes/package.json b/packages/zcli-themes/package.json index 51d892dd..0084c42b 100644 --- a/packages/zcli-themes/package.json +++ b/packages/zcli-themes/package.json @@ -19,7 +19,7 @@ "dependencies": { "@types/inquirer": "^8.0.0", "@types/ws": "^8.5.4", - "axios": "^0.27.2", + "axios": "^1.7.5", "chalk": "^4.1.2", "chokidar": "^3.5.3", "cors": "^2.8.5", diff --git a/packages/zcli-themes/src/commands/themes/import.ts b/packages/zcli-themes/src/commands/themes/import.ts index e1f39039..81012aa0 100644 --- a/packages/zcli-themes/src/commands/themes/import.ts +++ b/packages/zcli-themes/src/commands/themes/import.ts @@ -34,10 +34,9 @@ export default class Import extends Command { brandId = brandId || await getBrandId() const job = await createThemeImportJob(brandId) - const { readStream, removePackage } = await createThemePackage(themePath) - + const { file, removePackage } = await createThemePackage(themePath) try { - await uploadThemePackage(job, readStream) + await uploadThemePackage(job, file, path.basename(themePath)) } finally { removePackage() } diff --git a/packages/zcli-themes/src/commands/themes/update.ts b/packages/zcli-themes/src/commands/themes/update.ts index 4ebb4cdf..be33a0ac 100644 --- a/packages/zcli-themes/src/commands/themes/update.ts +++ b/packages/zcli-themes/src/commands/themes/update.ts @@ -34,10 +34,10 @@ export default class Update extends Command { themeId = themeId || await CliUx.ux.prompt('Theme ID') const job = await createThemeUpdateJob(themeId, replaceSettings) - const { readStream, removePackage } = await createThemePackage(themePath) + const { file, removePackage } = await createThemePackage(themePath) try { - await uploadThemePackage(job, readStream) + await uploadThemePackage(job, file, path.basename(themePath)) } finally { removePackage() } diff --git a/packages/zcli-themes/src/lib/createThemePackage.test.ts b/packages/zcli-themes/src/lib/createThemePackage.test.ts index 82159668..be81941b 100644 --- a/packages/zcli-themes/src/lib/createThemePackage.test.ts +++ b/packages/zcli-themes/src/lib/createThemePackage.test.ts @@ -12,8 +12,8 @@ describe('createThemePackage', () => { const writeStreamStub = sinon.createStubInstance(fs.WriteStream) sinon.stub(fs, 'createWriteStream').returns(writeStreamStub) - const readStreamStub = sinon.createStubInstance(fs.ReadStream) - sinon.stub(fs, 'createReadStream').returns(readStreamStub) + const readFileSync = sinon.createStubInstance(Buffer) + sinon.stub(fs, 'readFileSync').returns(readFileSync) const unlinkSyncStub = sinon.stub(fs, 'unlinkSync') @@ -26,9 +26,9 @@ describe('createThemePackage', () => { finalize: sinon.stub() } as any) // eslint-disable-line @typescript-eslint/no-explicit-any - const { readStream, removePackage } = await createThemePackage.default('theme/path') + const { file, removePackage } = await createThemePackage.default('theme/path') - expect(readStream).to.instanceOf(fs.ReadStream) + expect(file).to.instanceOf(Buffer) removePackage() expect(unlinkSyncStub.called).to.equal(true) diff --git a/packages/zcli-themes/src/lib/createThemePackage.ts b/packages/zcli-themes/src/lib/createThemePackage.ts index 7fbc49f5..76215c30 100644 --- a/packages/zcli-themes/src/lib/createThemePackage.ts +++ b/packages/zcli-themes/src/lib/createThemePackage.ts @@ -3,11 +3,38 @@ import * as fs from 'fs' import * as archiver from 'archiver' type CreateThemePackage = { - readStream: fs.ReadStream, + file: Buffer, removePackage: () => void } -export const createZipArchive = () => archiver('zip') +export const createZipArchive = (pkgPath: string, themePath: string, pkgName: string) => { + const archive = archiver('zip') + + return new Promise((resolve, reject) => { + const output = fs.createWriteStream(pkgPath) + + output.on('error', (err) => { + reject(err) + }) + + output.on('close', () => { + resolve(archive) + }) + + archive.directory(`${themePath}/assets`, `${pkgName}/assets`) + archive.directory(`${themePath}/settings`, `${pkgName}/settings`) + archive.directory(`${themePath}/templates`, `${pkgName}/templates`) + archive.directory(`${themePath}/translations`, `${pkgName}/translations`) + archive.file(`${themePath}/manifest.json`, { name: `${pkgName}/manifest.json` }) + archive.file(`${themePath}/script.js`, { name: `${pkgName}/script.js` }) + archive.file(`${themePath}/style.css`, { name: `${pkgName}/style.css` }) + archive.file(`${themePath}/thumbnail.png`, { name: `${pkgName}/thumbnail.png` }) + + archive.pipe(output) + + archive.finalize() + }) +} export default async function createThemePackage (themePath: string): Promise { CliUx.ux.action.start('Creating theme package') @@ -15,26 +42,13 @@ export default async function createThemePackage (themePath: string): Promise fs.unlinkSync(pkgPath) } } diff --git a/packages/zcli-themes/src/lib/uploadThemePackage.test.ts b/packages/zcli-themes/src/lib/uploadThemePackage.test.ts index 9622465a..947ce3fd 100644 --- a/packages/zcli-themes/src/lib/uploadThemePackage.test.ts +++ b/packages/zcli-themes/src/lib/uploadThemePackage.test.ts @@ -4,8 +4,6 @@ import * as axios from 'axios' import { request } from '@zendesk/zcli-core' import uploadThemePackage, { themeSizeLimit } from './uploadThemePackage' import * as errors from '@oclif/core/lib/errors' -import * as fs from 'fs' -import * as FormData from 'form-data' const job = { id: '9999', @@ -28,21 +26,22 @@ describe('uploadThemePackage', () => { }) it('calls the job upload endpoint with the correct payload and returns the job', async () => { - const readStreamStub = sinon.createStubInstance(fs.ReadStream) + const file = Buffer.from('file content') + const requestStub = sinon.stub(request, 'requestAPI') - await uploadThemePackage(job, readStreamStub) + await uploadThemePackage(job, file, 'filename') expect(requestStub.calledWith('upload/url', sinon.match({ method: 'POST', - data: sinon.match.instanceOf(FormData), + data: sinon.match.instanceOf(Buffer), maxBodyLength: themeSizeLimit, maxContentLength: themeSizeLimit }))).to.equal(true) }) it('errors when the upload fails', async () => { - const readStreamStub = sinon.createStubInstance(fs.ReadStream) + const file = Buffer.from('file content') const requestStub = sinon.stub(request, 'requestAPI') const errorStub = sinon.stub(errors, 'error').callThrough() const error = new axios.AxiosError('Network error') @@ -50,7 +49,7 @@ describe('uploadThemePackage', () => { requestStub.throws(error) try { - await uploadThemePackage(job, readStreamStub) + await uploadThemePackage(job, file, 'filename') } catch { expect(errorStub.calledWith(error)).to.equal(true) } diff --git a/packages/zcli-themes/src/lib/uploadThemePackage.ts b/packages/zcli-themes/src/lib/uploadThemePackage.ts index 3361a2ef..a84254b3 100644 --- a/packages/zcli-themes/src/lib/uploadThemePackage.ts +++ b/packages/zcli-themes/src/lib/uploadThemePackage.ts @@ -1,6 +1,5 @@ import type { PendingJob } from '../types' import { CliUx } from '@oclif/core' -import * as fs from 'fs' import * as FormData from 'form-data' import * as axios from 'axios' import { request } from '@zendesk/zcli-core' @@ -8,7 +7,7 @@ import { error } from '@oclif/core/lib/errors' export const themeSizeLimit = 31457280 -export default async function uploadThemePackage (job: PendingJob, readStream: fs.ReadStream): Promise { +export default async function uploadThemePackage (job: PendingJob, file: Buffer, filename: string): Promise { CliUx.ux.action.start('Uploading theme package') const formData = new FormData() @@ -17,12 +16,15 @@ export default async function uploadThemePackage (job: PendingJob, readStream: f formData.append(key, job.data.upload.parameters[key]) } - formData.append('file', readStream) + formData.append('file', file, { + filename + }) try { await request.requestAPI(job.data.upload.url, { method: 'POST', - data: formData, + data: formData.getBuffer(), + headers: formData.getHeaders(), maxBodyLength: themeSizeLimit, maxContentLength: themeSizeLimit }) diff --git a/packages/zcli-themes/tests/functional/delete.test.ts b/packages/zcli-themes/tests/functional/delete.test.ts index d5888c99..d66c7ae6 100644 --- a/packages/zcli-themes/tests/functional/delete.test.ts +++ b/packages/zcli-themes/tests/functional/delete.test.ts @@ -1,14 +1,32 @@ import { expect, test } from '@oclif/test' import DeleteCommand from '../../src/commands/themes/delete' import env from './env' +import * as sinon from 'sinon' describe('themes:delete', function () { + let fetchStub: sinon.SinonStub + + beforeEach(() => { + fetchStub = sinon.stub(global, 'fetch') + }) + + afterEach(() => { + fetchStub.restore() + }) + describe('successful deletion', () => { const success = test .env(env) - .nock('https://z3ntest.zendesk.com', api => api - .delete('/api/v2/guide/theming/themes/1234') - .reply(204)) + .do(() => { + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/guide/theming/themes/1234', + method: 'DELETE' + })).resolves({ + status: 204, + ok: true, + text: () => Promise.resolve('') + }) + }) success .stdout() @@ -28,14 +46,21 @@ describe('themes:delete', function () { describe('delete failure', () => { test .env(env) - .nock('https://z3ntest.zendesk.com', api => api - .delete('/api/v2/guide/theming/themes/1234') - .reply(400, { - errors: [{ - code: 'ThemeNotFound', - title: 'Invalid id' - }] - })) + .do(() => { + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/guide/theming/themes/1234', + method: 'DELETE' + })).resolves({ + status: 400, + ok: false, + text: () => Promise.resolve(JSON.stringify({ + errors: [{ + code: 'ThemeNotFound', + title: 'Invalid id' + }] + })) + }) + }) .stderr() .it('should report delete errors', async ctx => { try { diff --git a/packages/zcli-themes/tests/functional/import.test.ts b/packages/zcli-themes/tests/functional/import.test.ts index 5c6830e4..df7a7d95 100644 --- a/packages/zcli-themes/tests/functional/import.test.ts +++ b/packages/zcli-themes/tests/functional/import.test.ts @@ -1,7 +1,7 @@ import type { Job } from '../../../zcli-themes/src/types' import { expect, test } from '@oclif/test' +import * as sinon from 'sinon' import * as path from 'path' -import * as nock from 'nock' import ImportCommand from '../../src/commands/themes/import' import env from './env' @@ -19,22 +19,46 @@ describe('themes:import', function () { } } + let fetchStub: sinon.SinonStub + + beforeEach(() => { + fetchStub = sinon.stub(global, 'fetch') + }) + + afterEach(() => { + fetchStub.restore() + }) + describe('successful import', () => { const success = test .env(env) - .nock('https://z3ntest.zendesk.com', api => { - api - .post('/api/v2/guide/theming/jobs/themes/imports') - .reply(202, { job }) + .do(() => { + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/guide/theming/jobs/themes/imports', + method: 'POST' + })).resolves({ + status: 202, + ok: true, + text: () => Promise.resolve(JSON.stringify({ job })) + }) - api - .get('/api/v2/guide/theming/jobs/9999') - .reply(200, { job: { ...job, status: 'completed' } }) - }) - .nock('https://s3.com', (api) => { - api - .post('/upload/path') - .reply(200) + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/guide/theming/jobs/9999', + method: 'GET' + })).resolves({ + status: 200, + ok: true, + text: () => Promise.resolve(JSON.stringify({ job: { ...job, status: 'completed' } })) + }) + + fetchStub.withArgs(sinon.match({ + url: 'https://s3.com/upload/path', + method: 'POST' + })).resolves({ + status: 200, + ok: true, + text: () => Promise.resolve('') + }) }) success @@ -56,15 +80,20 @@ describe('themes:import', function () { test .stderr() .env(env) - .nock('https://z3ntest.zendesk.com', api => { - api - .post('/api/v2/guide/theming/jobs/themes/imports') - .reply(400, { + .do(() => { + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/guide/theming/jobs/themes/imports', + method: 'POST' + })).resolves({ + status: 400, + ok: false, + text: () => Promise.resolve(JSON.stringify({ errors: [{ code: 'TooManyThemes', title: 'Maximum number of allowed themes reached' }] - }) + })) + }) }) .it('should report errors when creating the import job fails', async (ctx) => { try { @@ -73,21 +102,28 @@ describe('themes:import', function () { expect(ctx.stderr).to.contain('!') expect(error.message).to.contain('TooManyThemes') expect(error.message).to.contain('Maximum number of allowed themes reached') - } finally { - nock.cleanAll() } }) test .env(env) - .nock('https://z3ntest.zendesk.com', api => { - api - .post('/api/v2/guide/theming/jobs/themes/imports') - .reply(202, { job }) + .do(() => { + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/guide/theming/jobs/themes/imports', + method: 'POST' + })).resolves({ + status: 202, + ok: true, + text: () => Promise.resolve(JSON.stringify({ job })) + }) - api - .get('/api/v2/guide/theming/jobs/9999') - .reply(200, { + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/guide/theming/jobs/9999', + method: 'GET' + })).resolves({ + status: 200, + ok: true, + text: () => Promise.resolve(JSON.stringify({ job: { ...job, status: 'failed', @@ -109,12 +145,17 @@ describe('themes:import', function () { } ] } - }) - }) - .nock('https://s3.com', (api) => { - api - .post('/upload/path') - .reply(200) + })) + }) + + fetchStub.withArgs(sinon.match({ + url: 'https://s3.com/upload/path', + method: 'POST' + })).resolves({ + status: 200, + ok: true, + text: () => Promise.resolve('') + }) }) .it('should report validation errors', async () => { try { @@ -124,8 +165,6 @@ describe('themes:import', function () { expect(error.message).to.contain('Template(s) with syntax error(s)') expect(error.message).to.contain('Validation error') expect(error.message).to.contain("'post_form' does not exist") - } finally { - nock.cleanAll() } }) }) diff --git a/packages/zcli-themes/tests/functional/list.test.ts b/packages/zcli-themes/tests/functional/list.test.ts index e13d4ced..af633238 100644 --- a/packages/zcli-themes/tests/functional/list.test.ts +++ b/packages/zcli-themes/tests/functional/list.test.ts @@ -1,18 +1,36 @@ import { expect, test } from '@oclif/test' +import * as sinon from 'sinon' import ListCommand from '../../src/commands/themes/list' import env from './env' describe('themes:list', function () { + let fetchStub: sinon.SinonStub + + beforeEach(() => { + fetchStub = sinon.stub(global, 'fetch') + }) + + afterEach(() => { + fetchStub.restore() + }) + describe('successful list', () => { const success = test .env(env) - .nock('https://z3ntest.zendesk.com', api => api - .get('/api/v2/guide/theming/themes?brand_id=1111') - .reply(200, { themes: [] })) + .do(() => { + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/guide/theming/themes?brand_id=1111', + method: 'GET' + })).resolves({ + status: 200, + ok: true, + text: () => Promise.resolve(JSON.stringify({ themes: [] })) + }) + }) success .stdout() - .it('should display success message when thes themes are listed successfully', async ctx => { + .it('should display success message when the themes are listed successfully', async ctx => { await ListCommand.run(['--brandId', '1111']) expect(ctx.stdout).to.contain('Themes listed successfully []') }) @@ -28,14 +46,21 @@ describe('themes:list', function () { describe('list failure', () => { test .env(env) - .nock('https://z3ntest.zendesk.com', api => api - .get('/api/v2/guide/theming/themes?brand_id=1111') - .reply(500, { - errors: [{ - code: 'InternalError', - title: 'Something went wrong' - }] - })) + .do(() => { + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/guide/theming/themes?brand_id=1111', + method: 'GET' + })).resolves({ + status: 500, + ok: false, + text: () => Promise.resolve(JSON.stringify({ + errors: [{ + code: 'InternalError', + title: 'Something went wrong' + }] + })) + }) + }) .stderr() .it('should report list errors', async ctx => { try { diff --git a/packages/zcli-themes/tests/functional/preview.test.ts b/packages/zcli-themes/tests/functional/preview.test.ts index 7b357131..61927ba4 100644 --- a/packages/zcli-themes/tests/functional/preview.test.ts +++ b/packages/zcli-themes/tests/functional/preview.test.ts @@ -1,8 +1,8 @@ import type { Manifest } from '../../../zcli-themes/src/types' import { expect, test } from '@oclif/test' +import * as sinon from 'sinon' import * as path from 'path' import * as fs from 'fs' -import * as nock from 'nock' import axios from 'axios' import { cloneDeep } from 'lodash' import PreviewCommand from '../../src/commands/themes/preview' @@ -10,6 +10,15 @@ import env from './env' describe('themes:preview', function () { const baseThemePath = path.join(__dirname, 'mocks/base_theme') + let fetchStub: sinon.SinonStub + + beforeEach(() => { + fetchStub = sinon.stub(global, 'fetch') + }) + + afterEach(() => { + fetchStub.restore() + }) describe('successful preview', () => { let server @@ -17,10 +26,15 @@ describe('themes:preview', function () { const preview = test .stdout() .env(env) - .nock('https://z3ntest.zendesk.com', api => { - api - .put('/hc/api/internal/theming/local_preview') - .reply(200) + .do(() => { + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/hc/api/internal/theming/local_preview', + method: 'PUT' + })).resolves({ + status: 200, + ok: true, + text: () => Promise.resolve('') + }) }) .do(async () => { server = await PreviewCommand.run([baseThemePath, '--bind', '0.0.0.0', '--port', '9999']) @@ -28,7 +42,6 @@ describe('themes:preview', function () { afterEach(() => { server.close() - nock.cleanAll() }) preview @@ -70,25 +83,29 @@ describe('themes:preview', function () { }) describe('validation errors', () => { - after(() => { - nock.cleanAll() - }) - test .stdout() .env(env) - .it('should report template errors', async (ctx) => { - nock('https://z3ntest.zendesk.com').put('/hc/api/internal/theming/local_preview').reply(400, { - template_errors: { - home_page: [{ - description: "'articles' does not exist", - line: 10, - column: 6, - length: 7 - }] - } + .do(() => { + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/hc/api/internal/theming/local_preview', + method: 'PUT' + })).resolves({ + status: 400, + ok: false, + text: () => Promise.resolve(JSON.stringify({ + template_errors: { + home_page: [{ + description: "'articles' does not exist", + line: 10, + column: 6, + length: 7 + }] + } + })) }) - + }) + .it('should report template errors', async (ctx) => { try { await PreviewCommand.run([baseThemePath]) expect(ctx.stdout).to.contain(`Validation error ${baseThemePath}/templates/home_page.hbs:10:6`) diff --git a/packages/zcli-themes/tests/functional/publish.test.ts b/packages/zcli-themes/tests/functional/publish.test.ts index ccb96370..900c2f18 100644 --- a/packages/zcli-themes/tests/functional/publish.test.ts +++ b/packages/zcli-themes/tests/functional/publish.test.ts @@ -1,14 +1,32 @@ import { expect, test } from '@oclif/test' +import * as sinon from 'sinon' import PublishCommand from '../../src/commands/themes/publish' import env from './env' describe('themes:publish', function () { + let fetchStub: sinon.SinonStub + + beforeEach(() => { + fetchStub = sinon.stub(global, 'fetch') + }) + + afterEach(() => { + fetchStub.restore() + }) + describe('successful publish', () => { const success = test .env(env) - .nock('https://z3ntest.zendesk.com', api => api - .post('/api/v2/guide/theming/themes/1234/publish') - .reply(200)) + .do(() => { + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/guide/theming/themes/1234/publish', + method: 'POST' + })).resolves({ + status: 200, + ok: true, + text: () => Promise.resolve('') + }) + }) success .stdout() @@ -28,14 +46,21 @@ describe('themes:publish', function () { describe('publish failure', () => { test .env(env) - .nock('https://z3ntest.zendesk.com', api => api - .post('/api/v2/guide/theming/themes/1234/publish') - .reply(400, { - errors: [{ - code: 'ThemeNotFound', - title: 'Invalid id' - }] - })) + .do(() => { + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/guide/theming/themes/1234/publish', + method: 'POST' + })).resolves({ + status: 400, + ok: false, + text: () => Promise.resolve(JSON.stringify({ + errors: [{ + code: 'ThemeNotFound', + title: 'Invalid id' + }] + })) + }) + }) .stderr() .it('should report publish errors', async ctx => { try { diff --git a/packages/zcli-themes/tests/functional/update.test.ts b/packages/zcli-themes/tests/functional/update.test.ts index 6d2c33f4..7a9102eb 100644 --- a/packages/zcli-themes/tests/functional/update.test.ts +++ b/packages/zcli-themes/tests/functional/update.test.ts @@ -1,9 +1,9 @@ import type { Job } from '../../../zcli-themes/src/types' import { expect, test } from '@oclif/test' import * as path from 'path' -import * as nock from 'nock' import UpdateCommand from '../../src/commands/themes/update' import env from './env' +import * as sinon from 'sinon' describe('themes:update', function () { const baseThemePath = path.join(__dirname, 'mocks/base_theme') @@ -18,23 +18,43 @@ describe('themes:update', function () { } } } + let fetchStub: sinon.SinonStub + + beforeEach(() => { + fetchStub = sinon.stub(global, 'fetch') + }) + + afterEach(() => { + fetchStub.restore() + }) describe('successful update', () => { const success = test .env(env) - .nock('https://z3ntest.zendesk.com', api => { - api - .post('/api/v2/guide/theming/jobs/themes/updates') - .reply(202, { job }) + .do(() => { + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/guide/theming/jobs/themes/updates' + })).resolves({ + status: 202, + ok: true, + text: () => Promise.resolve(JSON.stringify({ job })) + }) - api - .get('/api/v2/guide/theming/jobs/9999') - .reply(200, { job: { ...job, status: 'completed' } }) - }) - .nock('https://s3.com', (api) => { - api - .post('/upload/path') - .reply(200) + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/guide/theming/jobs/9999' + })).resolves({ + status: 200, + ok: true, + text: () => Promise.resolve(JSON.stringify({ job: { ...job, status: 'completed' } })) + }) + + fetchStub.withArgs(sinon.match({ + url: 'https://s3.com/upload/path' + })).resolves({ + status: 200, + ok: true, + text: () => Promise.resolve('') + }) }) success @@ -56,15 +76,19 @@ describe('themes:update', function () { test .stderr() .env(env) - .nock('https://z3ntest.zendesk.com', api => { - api - .post('/api/v2/guide/theming/jobs/themes/updates') - .reply(400, { + .do(() => { + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/guide/theming/jobs/themes/updates' + })).resolves({ + status: 400, + ok: false, + text: () => Promise.resolve(JSON.stringify({ errors: [{ code: 'TooManyThemes', title: 'Maximum number of allowed themes reached' }] - }) + })) + }) }) .it('should report errors when creating the update job fails', async (ctx) => { try { @@ -73,21 +97,26 @@ describe('themes:update', function () { expect(ctx.stderr).to.contain('!') expect(error.message).to.contain('TooManyThemes') expect(error.message).to.contain('Maximum number of allowed themes reached') - } finally { - nock.cleanAll() } }) test .env(env) - .nock('https://z3ntest.zendesk.com', api => { - api - .post('/api/v2/guide/theming/jobs/themes/updates') - .reply(202, { job }) + .do(() => { + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/guide/theming/jobs/themes/updates' + })).resolves({ + status: 202, + ok: true, + text: () => Promise.resolve(JSON.stringify({ job })) + }) - api - .get('/api/v2/guide/theming/jobs/9999') - .reply(200, { + fetchStub.withArgs(sinon.match({ + url: 'https://z3ntest.zendesk.com/api/v2/guide/theming/jobs/9999' + })).resolves({ + status: 200, + ok: true, + text: () => Promise.resolve(JSON.stringify({ job: { ...job, status: 'failed', @@ -109,12 +138,16 @@ describe('themes:update', function () { } ] } - }) - }) - .nock('https://s3.com', (api) => { - api - .post('/upload/path') - .reply(200) + })) + }) + + fetchStub.withArgs(sinon.match({ + url: 'https://s3.com/upload/path' + })).resolves({ + status: 200, + ok: true, + text: () => Promise.resolve('') + }) }) .it('should report validation errors', async (ctx) => { try { @@ -124,8 +157,6 @@ describe('themes:update', function () { expect(error.message).to.contain('Template(s) with syntax error(s)') expect(error.message).to.contain('Validation error') expect(error.message).to.contain("'request_fosrm' does not exist") - } finally { - nock.cleanAll() } }) }) diff --git a/yarn.lock b/yarn.lock index db7c08a2..8d0c050c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2220,13 +2220,14 @@ at-least-node@^1.0.0: resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== -axios@^0.27.2: - version "0.27.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" - integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== +axios@^1.7.5: + version "1.7.5" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.5.tgz#21eed340eb5daf47d29b6e002424b3e88c8c54b1" + integrity sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw== dependencies: - follow-redirects "^1.14.9" + follow-redirects "^1.15.6" form-data "^4.0.0" + proxy-from-env "^1.1.0" balanced-match@^1.0.0: version "1.0.2" @@ -3944,10 +3945,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.6.tgz#022e9218c637f9f3fc9c35ab9c9193f05add60b2" integrity sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ== -follow-redirects@^1.14.9: - version "1.15.1" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" - integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== +follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== foreground-child@^2.0.0: version "2.0.0" @@ -5788,7 +5789,7 @@ nmtree@^1.0.6: dependencies: commander "^2.11.0" -nock@^13.0.0, nock@^13.2.8: +nock@^13.0.0: version "13.2.9" resolved "https://registry.yarnpkg.com/nock/-/nock-13.2.9.tgz#4faf6c28175d36044da4cfa68e33e5a15086ad4c" integrity sha512-1+XfJNYF1cjGB+TKMWi29eZ0b82QOvQs2YoLNzbpWGqFMtRQHTa57osqdGj4FrFPgkO4D4AZinzUJR9VvW3QUA== @@ -5798,6 +5799,15 @@ nock@^13.0.0, nock@^13.2.8: lodash "^4.17.21" propagate "^2.0.0" +nock@^13.2.8: + version "13.5.5" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.5.5.tgz#cd1caaca281d42be17d51946367a3d53a6af3e78" + integrity sha512-XKYnqUrCwXC8DGG1xX4YH5yNIrlh9c065uaMZZHUoeUUINTOyt+x/G+ezYk0Ft6ExSREVIs+qBJDK503viTfFA== + dependencies: + debug "^4.1.0" + json-stringify-safe "^5.0.1" + propagate "^2.0.0" + node-abi@^3.3.0: version "3.22.0" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.22.0.tgz#00b8250e86a0816576258227edbce7bbe0039362" @@ -6678,6 +6688,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" @@ -7405,7 +7420,16 @@ stdout-stderr@^0.1.9: debug "^4.1.1" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -7469,7 +7493,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -8054,7 +8085,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -8072,6 +8103,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"