Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion src/commands/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,39 @@ export const apiCommand = async (apiMethodName: string, options: OptionValues, c

let payload
if (options.data) {
payload = typeof options.data === 'string' ? JSON.parse(options.data) : options.data
if (typeof options.data === 'string') {
try {
payload = JSON.parse(options.data)
} catch {
const received = options.data.length > 80 ? `${options.data.slice(0, 80)}…` : options.data
return logAndThrowError(
`Invalid JSON provided to the ${chalk.cyanBright('--data')} flag.
Received: ${received}
The --data flag expects a JSON object of API parameters, e.g. --data '{"site_id":"123456"}'.
Note: key=value pairs are not accepted; use JSON syntax instead.`,
)
}
} else {
payload = options.data
}
} else {
payload = {}
}
try {
const apiResponse = await apiMethod(payload)
logJson(apiResponse)
} catch (error_) {
if (error_ instanceof Error && error_.message.includes('Missing required path variable')) {
const apiMethods = methods as { operationId: string; parameters: { path?: Record<string, unknown> } }[]
const pathVariables = apiMethods.find((method) => method.operationId === apiMethodName)?.parameters.path ?? {}
const requiredNames = Object.keys(pathVariables).join(', ')
return logAndThrowError(
`${error_.message}
The ${chalk.cyanBright('--data')} flag must include the path variable(s) required by ${apiMethodName}${
requiredNames ? `: ${requiredNames}` : ''
}, e.g. --data '{"site_id":"123456"}'`,
)
}
return logAndThrowError(error_)
}
}
66 changes: 66 additions & 0 deletions tests/unit/commands/api/api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { OptionValues } from 'commander'
import { beforeEach, describe, expect, test, vi } from 'vitest'

vi.mock('../../../../src/utils/telemetry/report-error.js', () => ({
reportError: vi.fn().mockResolvedValue(undefined),
}))

import { apiCommand } from '../../../../src/commands/api/api.js'
import type BaseCommand from '../../../../src/commands/base-command.js'

const getSite = vi.fn()

const command = { netlify: { api: { getSite } } } as unknown as BaseCommand

const runApi = async (data: string) => apiCommand('getSite', { data } as OptionValues, command)

const captureError = async (data: string): Promise<Error> => {
try {
await runApi(data)
} catch (error) {
return error as Error
}
throw new Error('expected apiCommand to throw')
}

describe('apiCommand --data parsing', () => {
beforeEach(() => {
getSite.mockReset()
})

test('rejects key=value input with an error naming --data', async () => {
await expect(runApi('site_id=123')).rejects.toThrowError(/--data/)
expect(getSite).not.toHaveBeenCalled()
})

test('error echoes the offending input and a concrete JSON example', async () => {
const error = await captureError('site_id=123')
expect(error.message).toContain('site_id=123')
expect(error.message).toContain(`--data '{"site_id":"123456"}'`)
expect(error.message).toContain('key=value')
expect(error.message).not.toContain('SyntaxError')
})

test('truncates long offending input in the error message', async () => {
const longInput = `site_id=${'9'.repeat(200)}`
const error = await captureError(longInput)
expect(error.message).not.toContain(longInput)
expect(error.message).toContain(longInput.slice(0, 80))
})

test('still accepts a valid JSON object string', async () => {
getSite.mockResolvedValue({ id: '123456' })

await runApi('{"site_id":"123456"}')

expect(getSite).toHaveBeenCalledWith({ site_id: '123456' })
})

test('names --data and the required variable when the API reports a missing path variable', async () => {
getSite.mockRejectedValue(new Error("Missing required path variable 'site_id'"))

const error = await captureError('{"other":"value"}')
expect(error.message).toContain('--data')
expect(error.message).toContain('site_id')
})
})
Loading