Skip to content

Commit 8e99145

Browse files
authored
Fix contractions in REST API CLI examples (#56178)
1 parent f143cfb commit 8e99145

File tree

2 files changed

+181
-3
lines changed

2 files changed

+181
-3
lines changed

src/rest/components/get-rest-code-samples.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ import { stringify } from 'javascript-stringify'
44
import type { CodeSample, Operation } from '@/rest/components/types'
55
import { type VersionItem } from '@/frame/components/context/MainContext'
66

7+
// Helper function to escape shell values containing single quotes (contractions)
8+
// This prevents malformed shell commands when contractions like "there's" are used
9+
function escapeShellValue(value: string): string {
10+
// Replace single quotes with '\'' to properly escape them in shell commands
11+
return value.replace(/'/g, "'\\''")
12+
}
13+
714
type CodeExamples = Record<string, any>
815

916
// If the content type is application/x-www-form-urlencoded the format of
@@ -77,10 +84,12 @@ export function getShellExample(
7784
if (bodyParameters && typeof bodyParameters === 'object' && !Array.isArray(bodyParameters)) {
7885
const paramNames = Object.keys(bodyParameters)
7986
paramNames.forEach((elem) => {
80-
requestBodyParams = `${requestBodyParams} ${CURL_CONTENT_TYPE_MAPPING[contentType]} '${elem}=${bodyParameters[elem]}'`
87+
const escapedValue = escapeShellValue(String(bodyParameters[elem]))
88+
requestBodyParams = `${requestBodyParams} ${CURL_CONTENT_TYPE_MAPPING[contentType]} '${elem}=${escapedValue}'`
8189
})
8290
} else {
83-
requestBodyParams = `${CURL_CONTENT_TYPE_MAPPING[contentType]} "${bodyParameters}"`
91+
const escapedValue = escapeShellValue(String(bodyParameters))
92+
requestBodyParams = `${CURL_CONTENT_TYPE_MAPPING[contentType]} "${escapedValue}"`
8493
}
8594
}
8695
}
@@ -210,7 +219,9 @@ function handleSingleParameter(
210219
separator = ''
211220
}
212221
if (typeof value === 'string') {
213-
cliLine += ` -f "${keyString}${separator}${value}"`
222+
// Escape single quotes in string values to prevent shell command issues with contractions
223+
const escapedValue = escapeShellValue(value)
224+
cliLine += ` -f '${keyString}${separator}${escapedValue}'`
214225
} else if (typeof value === 'number' || typeof value === 'boolean' || value === null) {
215226
cliLine += ` -F "${keyString}${separator}${value}"`
216227
} else if (Array.isArray(value)) {

src/rest/tests/cli-examples.js

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { describe, expect, test } from 'vitest'
2+
3+
import { getGHExample, getShellExample } from '../components/get-rest-code-samples.ts'
4+
5+
describe('CLI examples generation', () => {
6+
const mockOperation = {
7+
verb: 'patch',
8+
requestPath: '/repos/{owner}/{repo}/code-scanning/alerts/{alert_number}',
9+
serverUrl: 'https://api.github.com',
10+
subcategory: 'code-scanning',
11+
parameters: [],
12+
}
13+
14+
const mockVersions = {
15+
'free-pro-team@latest': {
16+
apiVersions: ['2022-11-28'],
17+
latestApiVersion: '2022-11-28',
18+
},
19+
}
20+
21+
test('GitHub CLI example properly escapes contractions in string values', () => {
22+
const codeSample = {
23+
request: {
24+
parameters: {
25+
owner: 'OWNER',
26+
repo: 'REPO',
27+
alert_number: 'ALERT_NUMBER',
28+
},
29+
bodyParameters: {
30+
state: 'dismissed',
31+
dismissed_reason: 'false positive',
32+
dismissed_comment:
33+
"This alert is not actually correct, because there's a sanitizer included in the library.",
34+
},
35+
},
36+
}
37+
38+
const result = getGHExample(mockOperation, codeSample, 'free-pro-team@latest', mockVersions)
39+
40+
// Check that the contraction is properly escaped
41+
expect(result).toContain("there'\\''s")
42+
// Ensure the command is properly formatted
43+
expect(result).toContain('gh api')
44+
expect(result).toContain('--method PATCH')
45+
expect(result).toContain("-f 'dismissed_comment=")
46+
})
47+
48+
test('cURL example properly escapes contractions in JSON body', () => {
49+
const codeSample = {
50+
request: {
51+
parameters: {
52+
owner: 'OWNER',
53+
repo: 'REPO',
54+
alert_number: 'ALERT_NUMBER',
55+
},
56+
bodyParameters: {
57+
state: 'dismissed',
58+
dismissed_reason: 'false positive',
59+
dismissed_comment:
60+
"This alert is not actually correct, because there's a sanitizer included in the library.",
61+
},
62+
contentType: 'application/json',
63+
},
64+
}
65+
66+
const result = getShellExample(mockOperation, codeSample, 'free-pro-team@latest', mockVersions)
67+
68+
// Check that the JSON string is properly escaped
69+
expect(result).toContain("there'\\''s")
70+
expect(result).toContain('curl -L')
71+
expect(result).toContain('-X PATCH')
72+
})
73+
74+
test('GitHub CLI example handles multiple contractions', () => {
75+
const codeSample = {
76+
request: {
77+
parameters: {
78+
owner: 'OWNER',
79+
repo: 'REPO',
80+
},
81+
bodyParameters: {
82+
title: "Here's what's wrong with the code",
83+
body: "It's not working because there's an issue and we can't fix it",
84+
},
85+
},
86+
}
87+
88+
const mockSimpleOperation = {
89+
verb: 'post',
90+
requestPath: '/repos/{owner}/{repo}/issues',
91+
serverUrl: 'https://api.github.com',
92+
subcategory: 'issues',
93+
parameters: [],
94+
}
95+
96+
const result = getGHExample(
97+
mockSimpleOperation,
98+
codeSample,
99+
'free-pro-team@latest',
100+
mockVersions,
101+
)
102+
103+
// Check that all contractions are properly escaped
104+
expect(result).toContain("Here'\\''s what'\\''s")
105+
expect(result).toContain("It'\\''s not working")
106+
expect(result).toContain("there'\\''s an issue")
107+
expect(result).toContain("can'\\''t fix")
108+
})
109+
110+
test('GitHub CLI example handles values without contractions normally', () => {
111+
const codeSample = {
112+
request: {
113+
parameters: {
114+
owner: 'OWNER',
115+
repo: 'REPO',
116+
},
117+
bodyParameters: {
118+
state: 'dismissed',
119+
dismissed_reason: 'false positive',
120+
dismissed_comment:
121+
'This alert is not actually correct because there is a sanitizer included in the library.',
122+
},
123+
},
124+
}
125+
126+
const result = getGHExample(mockOperation, codeSample, 'free-pro-team@latest', mockVersions)
127+
128+
// Check that normal text is not modified
129+
expect(result).toContain('there is a sanitizer')
130+
expect(result).not.toContain("'\\''")
131+
expect(result).toContain('gh api')
132+
})
133+
134+
test('cURL example with form data properly escapes contractions', () => {
135+
const codeSample = {
136+
request: {
137+
parameters: {
138+
owner: 'OWNER',
139+
repo: 'REPO',
140+
},
141+
bodyParameters: {
142+
comment: "Here's my feedback on this PR",
143+
},
144+
contentType: 'application/x-www-form-urlencoded',
145+
},
146+
}
147+
148+
const mockSimpleOperation = {
149+
verb: 'post',
150+
requestPath: '/repos/{owner}/{repo}/pulls/{pull_number}/reviews',
151+
serverUrl: 'https://api.github.com',
152+
subcategory: 'pulls',
153+
parameters: [],
154+
}
155+
156+
const result = getShellExample(
157+
mockSimpleOperation,
158+
codeSample,
159+
'free-pro-team@latest',
160+
mockVersions,
161+
)
162+
163+
// Check that form data values are properly escaped
164+
expect(result).toContain("Here'\\''s my feedback")
165+
expect(result).toContain('--data-urlencode')
166+
})
167+
})

0 commit comments

Comments
 (0)