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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ hono request [file] [options]
- `-d, --data <data>` - Request body data
- `-H, --header <header>` - Custom headers (can be used multiple times)
- `-w, --watch` - Watch for changes and resend request
- `-e, --exclude` - Exclude protocol response headers in the output

**Examples:**

Expand Down
90 changes: 82 additions & 8 deletions src/commands/request/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { requestCommand } from './index.js'
describe('requestCommand', () => {
let program: Command
let consoleLogSpy: ReturnType<typeof vi.spyOn>
let consoleWarnSpy: ReturnType<typeof vi.spyOn>
let mockModules: any
let mockBuildAndImportApp: any

Expand Down Expand Up @@ -51,6 +52,7 @@ describe('requestCommand', () => {
program = new Command()
requestCommand(program)
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})

// Get mocked modules
mockModules = {
Expand All @@ -66,6 +68,7 @@ describe('requestCommand', () => {

afterEach(() => {
consoleLogSpy.mockRestore()
consoleWarnSpy.mockRestore()
vi.restoreAllMocks()
})

Expand All @@ -90,7 +93,7 @@ describe('requestCommand', () => {
JSON.stringify(
{
status: 200,
body: '{"message":"Hello"}',
body: { message: 'Hello' },
headers: { 'content-type': 'application/json' },
},
null,
Expand All @@ -99,7 +102,7 @@ describe('requestCommand', () => {
)
})

it('should handle GET request to specific file', async () => {
it('should handle GET request to specific file with watch option', async () => {
const mockApp = new Hono()
mockApp.get('/', (c) => c.json({ message: 'Hello' }))

Expand All @@ -120,7 +123,7 @@ describe('requestCommand', () => {
JSON.stringify(
{
status: 200,
body: '{"message":"Hello"}',
body: { message: 'Hello' },
headers: { 'content-type': 'application/json' },
},
null,
Expand Down Expand Up @@ -158,7 +161,7 @@ describe('requestCommand', () => {
const expectedOutput = JSON.stringify(
{
status: 201,
body: '{"received":"test data"}',
body: { received: 'test data' },
headers: { 'content-type': 'application/json', 'x-custom-header': 'test-value' },
},
null,
Expand Down Expand Up @@ -195,7 +198,7 @@ describe('requestCommand', () => {
const expectedOutput = JSON.stringify(
{
status: 200,
body: '{"message":"Default app"}',
body: { message: 'Default app' },
headers: { 'content-type': 'application/json' },
},
null,
Expand Down Expand Up @@ -233,7 +236,9 @@ describe('requestCommand', () => {
JSON.stringify(
{
status: 200,
body: '{"auth":"Bearer token123"}',
body: {
auth: 'Bearer token123',
},
headers: { 'content-type': 'application/json' },
},
null,
Expand Down Expand Up @@ -273,7 +278,7 @@ describe('requestCommand', () => {
JSON.stringify(
{
status: 200,
body: '{"auth":"Bearer token456","userAgent":"TestClient/1.0","custom":"custom-value"}',
body: { auth: 'Bearer token456', userAgent: 'TestClient/1.0', custom: 'custom-value' },
headers: { 'content-type': 'application/json' },
},
null,
Expand Down Expand Up @@ -328,12 +333,81 @@ describe('requestCommand', () => {
JSON.stringify(
{
status: 200,
body: '{"success":true}',
body: { success: true },
headers: { 'content-type': 'application/json' },
},
null,
2
)
)
})

it('should handle HTML response', async () => {
const mockApp = new Hono()
const htmlContent = '<h1>Hello World</h1>'
mockApp.get('/html', (c) => c.html(htmlContent))
setupBasicMocks('test-app.js', mockApp)
await program.parseAsync(['node', 'test', 'request', '-P', '/html', 'test-app.js'])
expect(consoleLogSpy).toHaveBeenCalledWith(
JSON.stringify(
{
status: 200,
body: htmlContent,
headers: { 'content-type': 'text/html; charset=UTF-8' },
},
null,
2
)
)
})

it('should handle XML response', async () => {
const mockApp = new Hono()
const xmlContent = '<root><message>Hello</message></root>'
mockApp.get('/xml', (c) => c.body(xmlContent, 200, { 'Content-Type': 'application/xml' }))
setupBasicMocks('test-app.js', mockApp)
await program.parseAsync(['node', 'test', 'request', '-P', '/xml', 'test-app.js'])
expect(consoleLogSpy).toHaveBeenCalledWith(
JSON.stringify(
{
status: 200,
body: xmlContent,
headers: { 'content-type': 'application/xml' },
},
null,
2
)
)
})

it('should warn on binary PNG response', async () => {
const mockApp = new Hono()
const pngData = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 0])
mockApp.get('/image.png', (c) => c.body(pngData.buffer, 200, { 'Content-Type': 'image/png' }))
setupBasicMocks('test-app.js', mockApp)
await program.parseAsync(['node', 'test', 'request', '-P', '/image.png', 'test-app.js'])
expect(consoleWarnSpy).toHaveBeenCalledWith('Binary output can mess up your terminal.')
expect(consoleLogSpy).not.toHaveBeenCalled()
})

it('should warn on binary PDF response', async () => {
const mockApp = new Hono()
const pdfData = new Uint8Array([37, 80, 68, 70, 45, 49, 46, 55, 0, 0, 0, 0])
mockApp.get('/document.pdf', (c) =>
c.body(pdfData.buffer, 200, { 'Content-Type': 'application/pdf' })
)
setupBasicMocks('test-app.js', mockApp)
await program.parseAsync(['node', 'test', 'request', '-P', '/document.pdf', 'test-app.js'])
expect(consoleWarnSpy).toHaveBeenCalledWith('Binary output can mess up your terminal.')
expect(consoleLogSpy).not.toHaveBeenCalled()
})

it('should exclude protocol headers when --exclude is used', async () => {
const mockApp = new Hono()
const jsonBody = { message: 'Success' }
mockApp.get('/data', (c) => c.json(jsonBody))
setupBasicMocks('test-app.js', mockApp)
await program.parseAsync(['node', 'test', 'request', '-P', '/data', '--exclude', 'test-app.js'])
expect(consoleLogSpy).toHaveBeenCalledWith(JSON.stringify(jsonBody, null, 2))
})
})
62 changes: 59 additions & 3 deletions src/commands/request/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface RequestOptions {
header?: string[]
path?: string
watch: boolean
exclude: boolean
}

export function requestCommand(program: Command) {
Expand All @@ -23,6 +24,7 @@ export function requestCommand(program: Command) {
.option('-X, --method <method>', 'HTTP method', 'GET')
.option('-d, --data <data>', 'Request body data')
.option('-w, --watch', 'Watch for changes and resend request', false)
.option('-e, --exclude', 'Exclude protocol response headers in the output', false)
.option(
'-H, --header <header>',
'Custom headers',
Expand All @@ -37,7 +39,27 @@ export function requestCommand(program: Command) {
const buildIterator = getBuildIterator(file, watch)
for await (const app of buildIterator) {
const result = await executeRequest(app, path, options)
console.log(JSON.stringify(result, null, 2))
const outputBody = formatResponseBody(
result.body,
result.headers['content-type'],
options.exclude
)
const buffer = await result.response.clone().arrayBuffer()
if (isBinaryResponse(buffer)) {
console.warn('Binary output can mess up your terminal.')
return
}
if (options.exclude) {
console.log(outputBody)
} else {
console.log(
JSON.stringify(
{ status: result.status, body: outputBody, headers: result.headers },
null,
2
)
)
}
}
})
}
Expand Down Expand Up @@ -77,7 +99,7 @@ export async function executeRequest(
app: Hono,
requestPath: string,
options: RequestOptions
): Promise<{ status: number; body: string; headers: Record<string, string> }> {
): Promise<{ status: number; body: string; headers: Record<string, string>; response: Response }> {
// Build request
const url = new URL(requestPath, 'http://localhost')
const requestInit: RequestInit = {
Expand Down Expand Up @@ -111,11 +133,45 @@ export async function executeRequest(
responseHeaders[key] = value
})

const body = await response.text()
const body = await response.clone().text()

return {
status: response.status,
body,
headers: responseHeaders,
response: response,
}
}

const formatResponseBody = (
responseBody: string,
contentType: string | undefined,
excludeOption: boolean
): string => {
switch (contentType) {
case 'application/json': // expect c.json(data) response
try {
const parsedJSON = JSON.parse(responseBody)
if (excludeOption) {
return JSON.stringify(parsedJSON, null, 2)
}
return parsedJSON
} catch {
console.error('Response indicated JSON content type but failed to parse JSON.')
return responseBody
}
default:
return responseBody
}
}

const isBinaryResponse = (buffer: ArrayBuffer): boolean => {
const view = new Uint8Array(buffer)
const len = Math.min(view.length, 2000)
for (let i = 0; i < len; i++) {
if (view[i] === 0) {
return true
}
}
return false
}
Loading