Skip to content

Commit

Permalink
Add a module to poll the job status
Browse files Browse the repository at this point in the history
  • Loading branch information
luis-almeida committed Jun 5, 2023
1 parent e83ea32 commit 32afe39
Show file tree
Hide file tree
Showing 2 changed files with 177 additions and 0 deletions.
111 changes: 111 additions & 0 deletions packages/zcli-themes/src/lib/pollJobStatus.test.ts
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)
}
})
})
66 changes: 66 additions & 0 deletions packages/zcli-themes/src/lib/pollJobStatus.ts
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
}

0 comments on commit 32afe39

Please sign in to comment.