mirror of
https://github.com/foss42/apidash.git
synced 2025-12-02 02:39:19 +08:00
- Pre-filter unknown flags before ArgParser; keep positional args. - Tolerate non-request flags: -v/--verbose, --connect-timeout, --retry, --output, --compressed, -i/--include, --globoff. - Auth: support --oauth2-bearer; map to - Authorization only if absent. - Cookies: parse -b/--cookie; accept -c/--cookie-jar (ignored for request). - URL: prefer first http(s) positional when --url missing; quote cleaning. - Data: merge data-urlencode → data-raw → data-binary → data; default POST when body/form present; HEAD remains HEAD. - Forms: parse -F entries; auto-set multipart Content-Type if missing. - Headers: robust -H parsing for multi-colon values. - toCurlString: deterministic order; fix continuation spacing; emit -d right after headers/form; place -k/-L at end. - Utils: normalize backslash-newlines/CRLF; remove stray '+'; shlex split. - Tests: add unknown flags, oauth2-bearer (and non-override), cookie-jar, verbose/timeout/retry/output tolerance, data merging order, HEAD+data, -A user-agent, -b filename. - Docs: add Dartdoc for utils; class docs present.
649 lines
16 KiB
Dart
649 lines
16 KiB
Dart
import 'dart:io';
|
|
import 'package:curl_parser/curl_parser.dart';
|
|
import 'package:seed/seed.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
final apiUri = Uri.parse('https://api.apidash.dev');
|
|
|
|
void main() {
|
|
test(
|
|
'parse an easy cURL',
|
|
() async {
|
|
expect(
|
|
Curl.parse('curl -X GET https://api.apidash.dev'),
|
|
Curl(
|
|
method: 'GET',
|
|
uri: apiUri,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'parse another easy cURL',
|
|
() async {
|
|
expect(
|
|
Curl.parse(
|
|
r"""curl --location --request GET 'https://dummyimage.com/150/92c952' \
|
|
--header 'user-agent: Dart/3.8 (dart:io)' \
|
|
--header 'accept-encoding: gzip' \
|
|
--header 'content-length: 0' \
|
|
--header 'host: dummyimage.com'"""),
|
|
Curl(
|
|
method: 'GET',
|
|
uri: Uri.parse('https://dummyimage.com/150/92c952'),
|
|
headers: {
|
|
'user-agent': 'Dart/3.8 (dart:io)',
|
|
'accept-encoding': 'gzip',
|
|
'content-length': '0',
|
|
'host': 'dummyimage.com'
|
|
},
|
|
location: true),
|
|
);
|
|
},
|
|
);
|
|
|
|
test('parse POST request with multipart/form-data', () {
|
|
const curl = r'''curl -X POST 'https://api.apidash.dev/io/img' \
|
|
-H 'Content-Type: multipart/form-data' \
|
|
-F "imfile=@/path/to/image.jpg" \
|
|
-F "token=john"
|
|
''';
|
|
|
|
expect(
|
|
Curl.parse(curl),
|
|
Curl(
|
|
method: 'POST',
|
|
uri: Uri.parse('https://api.apidash.dev/io/img'),
|
|
headers: {"Content-Type": "multipart/form-data"},
|
|
form: true,
|
|
formData: [
|
|
FormDataModel(
|
|
name: "imfile",
|
|
value: "/path/to/image.jpg",
|
|
type: FormDataType.file,
|
|
),
|
|
FormDataModel(
|
|
name: "token",
|
|
value: "john",
|
|
type: FormDataType.text,
|
|
)
|
|
],
|
|
),
|
|
);
|
|
});
|
|
|
|
test(
|
|
'parse POST request with multipart/form-data if not specified in headers',
|
|
() {
|
|
const curl = r'''curl -X POST 'https://api.apidash.dev/io/img' \
|
|
-F "imfile=@/path/to/image.jpg" \
|
|
-F "token=john"
|
|
''';
|
|
|
|
expect(
|
|
Curl.parse(curl),
|
|
Curl(
|
|
method: 'POST',
|
|
uri: Uri.parse('https://api.apidash.dev/io/img'),
|
|
headers: {"Content-Type": "multipart/form-data"},
|
|
form: true,
|
|
formData: [
|
|
FormDataModel(
|
|
name: "imfile",
|
|
value: "/path/to/image.jpg",
|
|
type: FormDataType.file,
|
|
),
|
|
FormDataModel(
|
|
name: "token",
|
|
value: "john",
|
|
type: FormDataType.text,
|
|
)
|
|
],
|
|
),
|
|
);
|
|
});
|
|
|
|
test(
|
|
'parse POST request with multipart/form-data but some other content type provided by user',
|
|
() {
|
|
const curl = r'''curl -X POST 'https://api.apidash.dev/io/img' \
|
|
-H 'content-type: some-data' \
|
|
-F "imfile=@/path/to/image.jpg" \
|
|
-F "token=john"
|
|
''';
|
|
|
|
expect(
|
|
Curl.parse(curl),
|
|
Curl(
|
|
method: 'POST',
|
|
uri: Uri.parse('https://api.apidash.dev/io/img'),
|
|
headers: {"content-type": "some-data"},
|
|
form: true,
|
|
formData: [
|
|
FormDataModel(
|
|
name: "imfile",
|
|
value: "/path/to/image.jpg",
|
|
type: FormDataType.file,
|
|
),
|
|
FormDataModel(
|
|
name: "token",
|
|
value: "john",
|
|
type: FormDataType.text,
|
|
)
|
|
],
|
|
),
|
|
);
|
|
});
|
|
|
|
test('parse POST request with x-www-form-urlencoded', () {
|
|
const curl = r'''curl -X 'POST' \
|
|
'https://api.apidash.dev/io/form' \
|
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
|
-d 'text=apidash&sep=%7C×=3'
|
|
''';
|
|
|
|
expect(
|
|
Curl.parse(curl),
|
|
Curl(
|
|
method: 'POST',
|
|
uri: Uri.parse('https://api.apidash.dev/io/form'),
|
|
headers: {"Content-Type": "application/x-www-form-urlencoded"},
|
|
data: 'text=apidash&sep=%7C×=3',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('should throw exception when multipart/form-data is not valid', () {
|
|
const curl = r'''curl -X POST https://api.apidash.dev/upload \
|
|
-F "invalid_format" \
|
|
-F "username=john"
|
|
''';
|
|
expect(() => Curl.parse(curl), throwsException);
|
|
});
|
|
|
|
test(
|
|
'Check quotes support for URL string',
|
|
() async {
|
|
expect(
|
|
Curl.parse('curl -X GET "https://api.apidash.dev"'),
|
|
Curl(method: 'GET', uri: apiUri),
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'parses two headers',
|
|
() async {
|
|
expect(
|
|
Curl.parse(
|
|
'curl -X GET https://api.apidash.dev -H "${HttpHeaders.contentTypeHeader}: ${ContentType.text}" -H "${HttpHeaders.authorizationHeader}: Bearer %token%"',
|
|
),
|
|
Curl(
|
|
method: 'GET',
|
|
uri: apiUri,
|
|
headers: {
|
|
HttpHeaders.contentTypeHeader: ContentType.text.toString(),
|
|
HttpHeaders.authorizationHeader: 'Bearer %token%',
|
|
},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'parses a lot of headers',
|
|
() async {
|
|
expect(
|
|
Curl.parse(
|
|
r'''curl -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36" \
|
|
-H "Upgrade-Insecure-Requests: 1" \
|
|
-H "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7" \
|
|
-H "Accept-Encoding: gzip, deflate, br" \
|
|
-H "Accept-Language: en,en-GB;q=0.9,en-US;q=0.8,ru;q=0.7" \
|
|
-H "Cache-Control: max-age=0" \
|
|
-H "Dnt: 1" \
|
|
-H "Sec-Ch-Ua: \"Google Chrome\";v=\"117\", \"Not;A=Brand\";v=\"8\", \"Chromium\";v=\"117\"" \
|
|
-H "Sec-Ch-Ua-Mobile: ?0" \
|
|
-H "Sec-Ch-Ua-Platform: \"macOS\"" \
|
|
-H "Sec-Fetch-Dest: document" \
|
|
-H "Sec-Fetch-Mode: navigate" \
|
|
-H "Sec-Fetch-Site: same-origin" \
|
|
-H "Sec-Fetch-User: ?1" https://apidash.dev''',
|
|
),
|
|
Curl(
|
|
method: 'GET',
|
|
uri: Uri.parse('https://apidash.dev'),
|
|
headers: {
|
|
'User-Agent':
|
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36',
|
|
'Upgrade-Insecure-Requests': '1',
|
|
'Accept':
|
|
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
|
'Accept-Encoding': 'gzip, deflate, br',
|
|
'Accept-Language': 'en,en-GB;q=0.9,en-US;q=0.8,ru;q=0.7',
|
|
'Cache-Control': 'max-age=0',
|
|
'Dnt': '1',
|
|
'Sec-Ch-Ua':
|
|
'"Google Chrome";v="117", "Not;A=Brand";v="8", "Chromium";v="117"',
|
|
'Sec-Ch-Ua-Mobile': '?0',
|
|
'Sec-Ch-Ua-Platform': '"macOS"',
|
|
'Sec-Fetch-Dest': 'document',
|
|
'Sec-Fetch-Mode': 'navigate',
|
|
'Sec-Fetch-Site': 'same-origin',
|
|
'Sec-Fetch-User': '?1'
|
|
},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'throw exception when parses wrong input',
|
|
() async {
|
|
expect(() => Curl.parse('1f'), throwsException);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'tryParse return null when parses wrong input',
|
|
() async {
|
|
expect(Curl.tryParse('1f'), null);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'tryParse success',
|
|
() async {
|
|
expect(
|
|
Curl.tryParse(
|
|
'curl -X GET https://api.apidash.dev -H "test: Agent"',
|
|
),
|
|
Curl(
|
|
method: 'GET',
|
|
uri: apiUri,
|
|
headers: {
|
|
'test': 'Agent',
|
|
},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
test('parses with common non-request flags without error', () {
|
|
const cmd = r"""curl \
|
|
--silent --compressed -o out.txt -i --globoff \
|
|
--url 'https://api.apidash.dev/echo' \
|
|
-H 'Accept: */*'""";
|
|
final curl = Curl.tryParse(cmd);
|
|
expect(curl, isNotNull);
|
|
expect(
|
|
curl,
|
|
Curl(
|
|
method: 'GET',
|
|
uri: Uri.parse('https://api.apidash.dev/echo'),
|
|
headers: {'Accept': '*/*'},
|
|
),
|
|
);
|
|
});
|
|
|
|
test('parses with verbose, connect-timeout, retry, and output flags', () {
|
|
const cmd = r"""curl -v \
|
|
--connect-timeout 10 \
|
|
--retry 3 \
|
|
--output response.json \
|
|
--url 'https://api.apidash.dev/echo' \
|
|
-H 'Content-Type: application/json' \
|
|
--data '{"x":1}'""";
|
|
final curl = Curl.tryParse(cmd);
|
|
expect(curl, isNotNull);
|
|
expect(
|
|
curl,
|
|
Curl(
|
|
method: 'POST',
|
|
uri: Uri.parse('https://api.apidash.dev/echo'),
|
|
headers: {'Content-Type': 'application/json'},
|
|
data: '{"x":1}',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('merges multiple data flags and defaults to POST', () {
|
|
const cmd = r"""curl \
|
|
--url 'https://api.apidash.dev/submit' \
|
|
--data-urlencode 'a=hello world' \
|
|
--data-raw 'b=2' \
|
|
--data-binary 'c=3' \
|
|
-H 'Content-Type: application/x-www-form-urlencoded'""";
|
|
final curl = Curl.parse(cmd);
|
|
expect(
|
|
curl,
|
|
Curl(
|
|
method: 'POST',
|
|
uri: Uri.parse('https://api.apidash.dev/submit'),
|
|
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
|
data: 'a=hello world&b=2&c=3',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('supports PATCH method', () {
|
|
const cmd = r"""curl -X PATCH \
|
|
'https://api.apidash.dev/resource/42' \
|
|
-H 'Content-Type: application/json' \
|
|
--data '{"status":"ok"}'""";
|
|
final curl = Curl.parse(cmd);
|
|
expect(
|
|
curl,
|
|
Curl(
|
|
method: 'PATCH',
|
|
uri: Uri.parse('https://api.apidash.dev/resource/42'),
|
|
headers: {'Content-Type': 'application/json'},
|
|
data: '{"status":"ok"}',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('merges data flags in defined order', () {
|
|
const cmd = r"""curl \
|
|
--url 'https://api.apidash.dev/submit' \
|
|
--data-urlencode 'a=1' \
|
|
--data-raw 'b=2' \
|
|
--data-binary 'c=3' \
|
|
--data 'd=4'""";
|
|
final curl = Curl.parse(cmd);
|
|
expect(curl.data, 'a=1&b=2&c=3&d=4');
|
|
expect(curl.method, 'POST');
|
|
});
|
|
|
|
test('HEAD with data stays HEAD (no implicit POST)', () {
|
|
const cmd = r"""curl -I \
|
|
'https://api.apidash.dev/whatever' \
|
|
--data 'should_be_ignored_for_method'""";
|
|
final curl = Curl.parse(cmd);
|
|
expect(curl.method, 'HEAD');
|
|
});
|
|
|
|
test('user agent via -A populates userAgent field', () {
|
|
const cmd = r"""curl -A 'MyApp/1.0' \
|
|
'https://api.apidash.dev/ua'""";
|
|
final curl = Curl.parse(cmd);
|
|
expect(
|
|
curl,
|
|
Curl(
|
|
method: 'GET',
|
|
uri: Uri.parse('https://api.apidash.dev/ua'),
|
|
userAgent: 'MyApp/1.0',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('cookie via -b with filename is tolerated', () {
|
|
const cmd = r"""curl -b cookies.txt \
|
|
'https://api.apidash.dev/echo'""";
|
|
final curl = Curl.parse(cmd);
|
|
expect(
|
|
curl,
|
|
Curl(
|
|
method: 'GET',
|
|
uri: Uri.parse('https://api.apidash.dev/echo'),
|
|
cookie: 'cookies.txt',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('maps --oauth2-bearer to Authorization header if absent', () {
|
|
const cmd = r"""curl --url 'https://api.apidash.dev/secure' \
|
|
--oauth2-bearer 'tok_123'""";
|
|
final curl = Curl.parse(cmd);
|
|
expect(
|
|
curl,
|
|
Curl(
|
|
method: 'GET',
|
|
uri: Uri.parse('https://api.apidash.dev/secure'),
|
|
headers: {'Authorization': 'Bearer tok_123'},
|
|
),
|
|
);
|
|
});
|
|
|
|
test('oauth2-bearer does not override existing Authorization', () {
|
|
const cmd = r"""curl \
|
|
--url 'https://api.apidash.dev/secure' \
|
|
-H 'Authorization: Bearer explicit' \
|
|
--oauth2-bearer 'ignored'""";
|
|
final curl = Curl.parse(cmd);
|
|
expect(
|
|
curl,
|
|
Curl(
|
|
method: 'GET',
|
|
uri: Uri.parse('https://api.apidash.dev/secure'),
|
|
headers: {'Authorization': 'Bearer explicit'},
|
|
),
|
|
);
|
|
});
|
|
|
|
test('chooses first http(s) positional URL if --url missing', () {
|
|
const cmd = r"""curl foo bar \
|
|
'https://api.apidash.dev/echo' \
|
|
baz""";
|
|
final curl = Curl.parse(cmd);
|
|
expect(
|
|
curl.uri,
|
|
Uri.parse('https://api.apidash.dev/echo'),
|
|
);
|
|
});
|
|
|
|
test('tolerates cookie-jar without affecting request import', () {
|
|
const cmd = r"""curl --url 'https://api.apidash.dev/echo' \
|
|
-c cookies.txt -b 'a=1'""";
|
|
final curl = Curl.parse(cmd);
|
|
expect(
|
|
curl,
|
|
Curl(
|
|
method: 'GET',
|
|
uri: Uri.parse('https://api.apidash.dev/echo'),
|
|
cookie: 'a=1',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('ignores unknown long flags safely', () {
|
|
const cmd = r"""curl --unknown-flag foo \
|
|
--url 'https://api.apidash.dev/echo' \
|
|
--still-unknown=bar \
|
|
-H 'Accept: */*'""";
|
|
final curl = Curl.tryParse(cmd);
|
|
expect(
|
|
curl,
|
|
Curl(
|
|
method: 'GET',
|
|
uri: Uri.parse('https://api.apidash.dev/echo'),
|
|
headers: {'Accept': '*/*'},
|
|
),
|
|
);
|
|
});
|
|
|
|
test('ignores unknown short flags safely', () {
|
|
const cmd = r"""curl -Z -Y \
|
|
'https://api.apidash.dev/echo' \
|
|
-H 'X-Test: 1'""";
|
|
final curl = Curl.tryParse(cmd);
|
|
expect(
|
|
curl,
|
|
Curl(
|
|
method: 'GET',
|
|
uri: Uri.parse('https://api.apidash.dev/echo'),
|
|
headers: {'X-Test': '1'},
|
|
),
|
|
);
|
|
});
|
|
|
|
test(
|
|
'HEAD 1',
|
|
() async {
|
|
expect(
|
|
Curl.parse(
|
|
r"""curl -I 'https://api.apidash.dev'""",
|
|
),
|
|
Curl(
|
|
method: 'HEAD',
|
|
uri: apiUri,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'HEAD 2',
|
|
() async {
|
|
expect(
|
|
Curl.parse(
|
|
r"""curl --head 'https://api.apidash.dev'""",
|
|
),
|
|
Curl(
|
|
method: 'HEAD',
|
|
uri: apiUri,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'GET 1',
|
|
() async {
|
|
expect(
|
|
Curl.parse(
|
|
r"""curl --url 'https://api.apidash.dev'""",
|
|
),
|
|
Curl(
|
|
method: 'GET',
|
|
uri: apiUri,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'GET 2',
|
|
() async {
|
|
expect(
|
|
Curl.parse(
|
|
r"""curl --url 'https://api.apidash.dev/country/data?code=US'""",
|
|
),
|
|
Curl(
|
|
method: 'GET',
|
|
uri: Uri.parse('https://api.apidash.dev/country/data?code=US'),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'GET 3',
|
|
() async {
|
|
expect(
|
|
Curl.parse(
|
|
r"""curl 'https://api.apidash.dev/country/data?code=IND'""",
|
|
),
|
|
Curl(
|
|
method: 'GET',
|
|
uri: Uri.parse('https://api.apidash.dev/country/data?code=IND'),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'GET with GitHub API',
|
|
() async {
|
|
expect(
|
|
Curl.parse(
|
|
r"""curl --url 'https://api.github.com/repos/foss42/apidash' \
|
|
--header 'User-Agent: Test Agent'""",
|
|
),
|
|
Curl(
|
|
method: 'GET',
|
|
uri: Uri.parse('https://api.github.com/repos/foss42/apidash'),
|
|
headers: {"User-Agent": "Test Agent"},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'GET with GitHub API and raw parameter',
|
|
() async {
|
|
expect(
|
|
Curl.parse(
|
|
r"""curl --url 'https://api.github.com/repos/foss42/apidash?raw=true' \
|
|
--header 'User-Agent: Test Agent'""",
|
|
),
|
|
Curl(
|
|
method: 'GET',
|
|
uri:
|
|
Uri.parse('https://api.github.com/repos/foss42/apidash?raw=true'),
|
|
headers: {"User-Agent": "Test Agent"},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'POST with text/plain content',
|
|
() async {
|
|
expect(
|
|
Curl.parse(
|
|
r"""curl --request POST \
|
|
--url 'https://api.apidash.dev/case/lower' \
|
|
--header 'Content-Type: text/plain' \
|
|
--data '{
|
|
"text": "I LOVE Flutter"
|
|
}'""",
|
|
),
|
|
Curl(
|
|
method: 'POST',
|
|
uri: Uri.parse('https://api.apidash.dev/case/lower'),
|
|
headers: {"Content-Type": "text/plain"},
|
|
data: r"""{
|
|
"text": "I LOVE Flutter"
|
|
}""",
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'POST with application/json content',
|
|
() async {
|
|
expect(
|
|
Curl.parse(
|
|
r"""curl --request POST \
|
|
--url 'https://api.apidash.dev/case/lower' \
|
|
--header 'Content-Type: application/json' \
|
|
--data '{
|
|
"text": "I LOVE Flutter",
|
|
"flag": null,
|
|
"male": true,
|
|
"female": false,
|
|
"no": 1.2,
|
|
"arr": ["null", "true", "false", null]
|
|
}'""",
|
|
),
|
|
Curl(
|
|
method: 'POST',
|
|
uri: Uri.parse('https://api.apidash.dev/case/lower'),
|
|
headers: {"Content-Type": "application/json"},
|
|
data: r"""{
|
|
"text": "I LOVE Flutter",
|
|
"flag": null,
|
|
"male": true,
|
|
"female": false,
|
|
"no": 1.2,
|
|
"arr": ["null", "true", "false", null]
|
|
}""",
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|