-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e83ea32
commit 32afe39
Showing
2 changed files
with
177 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import * as sinon from 'sinon' | ||
import { expect } from '@oclif/test' | ||
import * as axios from 'axios' | ||
import { request } from '@zendesk/zcli-core' | ||
import pollJobStatus from './pollJobStatus' | ||
import * as chalk from 'chalk' | ||
import * as errors from '@oclif/core/lib/errors' | ||
|
||
describe('pollJobStatus', () => { | ||
beforeEach(() => { | ||
sinon.restore() | ||
}) | ||
|
||
it('polls the jobs/{jobId} endpoint until the job is completed', async () => { | ||
const requestStub = sinon.stub(request, 'requestAPI') | ||
|
||
requestStub | ||
.onFirstCall() | ||
.returns(Promise.resolve({ data: { job: { status: 'pending' } } }) as axios.AxiosPromise) | ||
.onSecondCall() | ||
.returns(Promise.resolve({ data: { job: { status: 'pending' } } }) as axios.AxiosPromise) | ||
.onThirdCall() | ||
.returns(Promise.resolve({ data: { job: { status: 'completed', theme_id: '1234' } } }) as axios.AxiosPromise) | ||
|
||
await pollJobStatus('theme/path', '9999', 10) | ||
|
||
expect(requestStub.calledWith('/api/v2/guide/theming/jobs/9999')).to.equal(true) | ||
expect(requestStub.callCount).to.equal(3) | ||
}) | ||
|
||
it('times out after the specified number of retries', async () => { | ||
const requestStub = sinon.stub(request, 'requestAPI') | ||
const errorStub = sinon.stub(errors, 'error') | ||
|
||
requestStub | ||
.onFirstCall() | ||
.returns(Promise.resolve({ data: { job: { status: 'pending' } } }) as axios.AxiosPromise) | ||
.onSecondCall() | ||
.returns(Promise.resolve({ data: { job: { status: 'pending' } } }) as axios.AxiosPromise) | ||
.onThirdCall() | ||
.returns(Promise.resolve({ data: { job: { status: 'pending' } } }) as axios.AxiosPromise) | ||
|
||
await pollJobStatus('theme/path', '9999', 10, 3) | ||
|
||
expect(requestStub.callCount).to.equal(3) | ||
expect(errorStub.calledWith('Import job timed out')).to.equal(true) | ||
}) | ||
|
||
it('handles job errors', async () => { | ||
const requestStub = sinon.stub(request, 'requestAPI') | ||
const errorStub = sinon.stub(errors, 'error').callThrough() | ||
|
||
requestStub | ||
.onFirstCall() | ||
.returns(Promise.resolve({ data: { job: { status: 'pending' } } }) as axios.AxiosPromise) | ||
.onSecondCall() | ||
.returns(Promise.resolve({ | ||
data: { | ||
job: { | ||
status: 'failed', | ||
errors: [ | ||
{ | ||
message: 'Template(s) with syntax error(s)', | ||
code: 'InvalidTemplates', | ||
meta: { | ||
'templates/home_page.hbs': [ | ||
{ | ||
description: 'not possible to access `names` in `help_center.names`', | ||
line: 1, | ||
column: 45, | ||
length: 5 | ||
}, | ||
{ | ||
description: "'categoriess' does not exist", | ||
line: 21, | ||
column: 16, | ||
length: 11 | ||
} | ||
], | ||
'templates/new_request_page.hbs': [ | ||
{ | ||
description: "'request_fosrm' does not exist", | ||
line: 22, | ||
column: 6, | ||
length: 10 | ||
} | ||
] | ||
} | ||
} | ||
] | ||
} | ||
} | ||
}) as axios.AxiosPromise) | ||
|
||
try { | ||
await pollJobStatus('theme/path', '9999', 10, 2) | ||
} catch { | ||
expect(requestStub.callCount).to.equal(2) | ||
expect(errorStub.calledWithMatch('Template(s) with syntax error(s)')).to.equal(true) | ||
|
||
expect(errorStub.calledWithMatch(`${chalk.bold('Validation error')} theme/path/templates/home_page.hbs:1:45`)).to.equal(true) | ||
expect(errorStub.calledWithMatch('not possible to access `names` in `help_center.names`')).to.equal(true) | ||
|
||
expect(errorStub.calledWithMatch(`${chalk.bold('Validation error')} theme/path/templates/home_page.hbs:21:16`)).to.equal(true) | ||
expect(errorStub.calledWithMatch("'categoriess' does not exist")).to.equal(true) | ||
|
||
expect(errorStub.calledWithMatch(`${chalk.bold('Validation error')} theme/path/templates/new_request_page.hbs:22:6`)).to.equal(true) | ||
expect(errorStub.calledWithMatch("'request_fosrm' does not exist")).to.equal(true) | ||
} | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import type { Job, JobError, ValidationErrors } from '../types' | ||
import { CliUx } from '@oclif/core' | ||
import { error } from '@oclif/core/lib/errors' | ||
import { request } from '@zendesk/zcli-core' | ||
import * as chalk from 'chalk' | ||
|
||
export default async function pollJobStatus (themePath: string, jobId: string, interval = 1000, retries = 10): Promise<void> { | ||
CliUx.ux.action.start('Polling job status') | ||
|
||
while (retries) { | ||
// Delay issueing a retry | ||
await new Promise(resolve => setTimeout(resolve, interval)) | ||
|
||
const response = await request.requestAPI(`/api/v2/guide/theming/jobs/${jobId}`) | ||
const job: Job = response.data.job | ||
|
||
switch (job.status) { | ||
case 'pending': | ||
retries -= 1 | ||
break | ||
case 'completed': { | ||
CliUx.ux.action.stop('Ok') | ||
return | ||
} | ||
case 'failed': { | ||
// Although `data.job.errors` is an array, it usually contains | ||
// only one error at a time. Hence, we only need to handle the | ||
// first error in the array. | ||
const [error] = job.errors | ||
handleJobError(themePath, error) | ||
} | ||
} | ||
} | ||
|
||
error('Import job timed out') | ||
} | ||
|
||
function handleJobError (themePath: string, jobError: JobError): void { | ||
const { code, message, meta } = jobError | ||
const title = `${chalk.bold(code)} - ${message}` | ||
let details = '' | ||
|
||
switch (code) { | ||
case 'InvalidTemplates': | ||
case 'InvalidManifest': | ||
case 'InvalidTranslationFile': | ||
details = validationErrorsToString(themePath, meta as ValidationErrors) | ||
break | ||
default: | ||
details = JSON.stringify(meta) | ||
} | ||
|
||
error(`${title}\n${details}`) | ||
} | ||
|
||
export function validationErrorsToString (themePath: string, validationErrors: ValidationErrors): string { | ||
let string = '' | ||
|
||
for (const [template, errors] of Object.entries(validationErrors)) { | ||
for (const { line, column, description } of errors) { | ||
string += `\n${chalk.bold('Validation error')} ${themePath}/${template}${line && column ? `:${line}:${column}` : ''}\n ${description}\n` | ||
} | ||
} | ||
|
||
return string | ||
} |