✨ Improve HTTP client function
This commit is contained in:
@@ -24,7 +24,7 @@ export class HttpRequest {
|
|||||||
"headers": { [_: string]: string };
|
"headers": { [_: string]: string };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* json, formdata, urlencoded, text
|
* json, formdata, urlencoded, text, params, xml, html, javascript, binary
|
||||||
*/
|
*/
|
||||||
"bodyType"?: string;
|
"bodyType"?: string;
|
||||||
"body"?: any;
|
"body"?: any;
|
||||||
|
|||||||
@@ -10,6 +10,11 @@
|
|||||||
// @formdata - 表单数据(属性必须用逗号分隔)
|
// @formdata - 表单数据(属性必须用逗号分隔)
|
||||||
// @urlencoded - URL 编码格式(属性必须用逗号分隔)
|
// @urlencoded - URL 编码格式(属性必须用逗号分隔)
|
||||||
// @text - 纯文本内容
|
// @text - 纯文本内容
|
||||||
|
// @params - URL 参数(用于 GET 请求)
|
||||||
|
// @xml - XML 格式(固定 key: xml)
|
||||||
|
// @html - HTML 格式(固定 key: html)
|
||||||
|
// @javascript - JavaScript 格式(固定 key: javascript)
|
||||||
|
// @binary - 二进制文件(固定 key: binary,值格式:@file 路径)
|
||||||
//
|
//
|
||||||
// 3. 变量定义:
|
// 3. 变量定义:
|
||||||
// @var {
|
// @var {
|
||||||
@@ -70,7 +75,52 @@
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// 示例 5 - 带响应数据:
|
// 示例 5 - URL 参数请求:
|
||||||
|
// GET "http://api.example.com/users" {
|
||||||
|
// @params {
|
||||||
|
// page: 1,
|
||||||
|
// size: 20,
|
||||||
|
// keyword: "张三"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// 示例 6 - XML 请求:
|
||||||
|
// POST "http://api.example.com/soap" {
|
||||||
|
// content-type: "application/xml"
|
||||||
|
//
|
||||||
|
// @xml {
|
||||||
|
// xml: "<user><name>张三</name><age>25</age></user>"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// 示例 7 - HTML 请求:
|
||||||
|
// POST "http://api.example.com/render" {
|
||||||
|
// content-type: "text/html"
|
||||||
|
//
|
||||||
|
// @html {
|
||||||
|
// html: "<div><h1>标题</h1><p>内容</p></div>"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// 示例 8 - JavaScript 请求:
|
||||||
|
// POST "http://api.example.com/execute" {
|
||||||
|
// content-type: "application/javascript"
|
||||||
|
//
|
||||||
|
// @javascript {
|
||||||
|
// javascript: "function hello() { return 'Hello World'; }"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// 示例 9 - 二进制文件上传:
|
||||||
|
// POST "http://api.example.com/upload" {
|
||||||
|
// content-type: "application/octet-stream"
|
||||||
|
//
|
||||||
|
// @binary {
|
||||||
|
// binary: "@file E://Documents/avatar.png"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// 示例 10 - 带响应数据:
|
||||||
// POST "http://api.example.com/login" {
|
// POST "http://api.example.com/login" {
|
||||||
// @json {
|
// @json {
|
||||||
// username: "admin",
|
// username: "admin",
|
||||||
@@ -120,14 +170,13 @@ ResponseDeclaration {
|
|||||||
ResponseBlock
|
ResponseBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应状态:状态码(200 或 200-OK)或 "error" 关键字
|
// 响应状态:数字或数字-标识符组合(200 或 200-OK)或 "error" 关键字
|
||||||
// 数字开头的状态码作为一个整体 token
|
|
||||||
ResponseStatus {
|
ResponseStatus {
|
||||||
StatusCode |
|
(NumberLiteral ("-" identifier)?) |
|
||||||
@specialize[@name=ErrorStatus]<identifier, "error">
|
@specialize[@name=ErrorStatus]<identifier, "error">
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应时间:数字 + "ms" 作为一个整体 token
|
// 响应时间:直接使用 TimeValue token
|
||||||
ResponseTime {
|
ResponseTime {
|
||||||
TimeValue
|
TimeValue
|
||||||
}
|
}
|
||||||
@@ -168,7 +217,12 @@ AtRule {
|
|||||||
(JsonRule |
|
(JsonRule |
|
||||||
FormDataRule |
|
FormDataRule |
|
||||||
UrlEncodedRule |
|
UrlEncodedRule |
|
||||||
TextRule) ","?
|
TextRule |
|
||||||
|
ParamsRule |
|
||||||
|
XmlRule |
|
||||||
|
HtmlRule |
|
||||||
|
JavaScriptRule |
|
||||||
|
BinaryRule) ","?
|
||||||
}
|
}
|
||||||
|
|
||||||
// @json 块:JSON 格式请求体(属性必须用逗号分隔)
|
// @json 块:JSON 格式请求体(属性必须用逗号分隔)
|
||||||
@@ -195,6 +249,36 @@ TextRule {
|
|||||||
JsonBlock
|
JsonBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @params 块:URL 参数(用于 GET 请求,属性必须用逗号分隔)
|
||||||
|
ParamsRule {
|
||||||
|
@specialize[@name=ParamsKeyword]<AtKeyword, "@params">
|
||||||
|
JsonBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
// @xml 块:XML 格式请求体(固定 key: xml)
|
||||||
|
XmlRule {
|
||||||
|
@specialize[@name=XmlKeyword]<AtKeyword, "@xml">
|
||||||
|
XmlBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
// @html 块:HTML 格式请求体(固定 key: html)
|
||||||
|
HtmlRule {
|
||||||
|
@specialize[@name=HtmlKeyword]<AtKeyword, "@html">
|
||||||
|
HtmlBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
// @javascript 块:JavaScript 格式请求体(固定 key: javascript)
|
||||||
|
JavaScriptRule {
|
||||||
|
@specialize[@name=JavaScriptKeyword]<AtKeyword, "@javascript">
|
||||||
|
JavaScriptBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
// @binary 块:二进制文件(固定 key: binary,值格式:@file 路径)
|
||||||
|
BinaryRule {
|
||||||
|
@specialize[@name=BinaryKeyword]<AtKeyword, "@binary">
|
||||||
|
BinaryBlock
|
||||||
|
}
|
||||||
|
|
||||||
// 普通块结构(属性逗号可选,最多一个请求体)
|
// 普通块结构(属性逗号可选,最多一个请求体)
|
||||||
Block {
|
Block {
|
||||||
"{" blockContent? "}"
|
"{" blockContent? "}"
|
||||||
@@ -229,6 +313,30 @@ JsonProperty {
|
|||||||
":" jsonValue
|
":" jsonValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XML 块结构(可为空 {} 或必须包含 xml: value)
|
||||||
|
XmlBlock {
|
||||||
|
"{" (@specialize[@name=XmlKey]<identifier, "xml"> ":" jsonValue) "}" |
|
||||||
|
"{" "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTML 块结构(可为空 {} 或必须包含 html: value)
|
||||||
|
HtmlBlock {
|
||||||
|
"{" (@specialize[@name=HtmlKey]<identifier, "html"> ":" jsonValue) "}" |
|
||||||
|
"{" "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
// JavaScript 块结构(可为空 {} 或必须包含 javascript: value)
|
||||||
|
JavaScriptBlock {
|
||||||
|
"{" (@specialize[@name=JavaScriptKey]<identifier, "javascript"> ":" jsonValue) "}" |
|
||||||
|
"{" "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary 块结构(可为空 {} 或必须包含 binary: value)
|
||||||
|
BinaryBlock {
|
||||||
|
"{" (@specialize[@name=BinaryKey]<identifier, "binary"> ":" jsonValue) "}" |
|
||||||
|
"{" "}"
|
||||||
|
}
|
||||||
|
|
||||||
// 值
|
// 值
|
||||||
NumberLiteral {
|
NumberLiteral {
|
||||||
numberLiteralInner Unit?
|
numberLiteralInner Unit?
|
||||||
@@ -328,19 +436,14 @@ JsonNull { @specialize[@name=Null]<identifier, "null"> }
|
|||||||
"T" @digit @digit ":" @digit @digit ":" @digit @digit
|
"T" @digit @digit ":" @digit @digit ":" @digit @digit
|
||||||
}
|
}
|
||||||
|
|
||||||
// 状态码:纯数字或数字-字母组合(200, 200-OK, 404-Not-Found)
|
// 时间值:数字 + ms,作为一个整体 token
|
||||||
StatusCode {
|
TimeValue[isolate] {
|
||||||
@digit+ ("-" @asciiLetter (@asciiLetter | "-")*)?
|
|
||||||
}
|
|
||||||
|
|
||||||
// 时间值:数字 + ms(123ms)
|
|
||||||
TimeValue {
|
|
||||||
@digit+ "ms"
|
@digit+ "ms"
|
||||||
}
|
}
|
||||||
|
|
||||||
whitespace { @whitespace+ }
|
whitespace { @whitespace+ }
|
||||||
|
|
||||||
@precedence { Timestamp, TimeValue, StatusCode, numberLiteralInner, VariableRef, identifier, Unit }
|
@precedence { Timestamp, TimeValue, numberLiteralInner, VariableRef, identifier, Unit }
|
||||||
|
|
||||||
numberLiteralInner {
|
numberLiteralInner {
|
||||||
("+" | "-")? (@digit+ ("." @digit*)? | "." @digit+)
|
("+" | "-")? (@digit+ ("." @digit*)? | "." @digit+)
|
||||||
|
|||||||
@@ -0,0 +1,302 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { parser } from './http.parser';
|
||||||
|
|
||||||
|
describe('HTTP Grammar - 固定 Key 约束测试', () => {
|
||||||
|
|
||||||
|
function parseAndCheck(content: string, expectError: boolean = false) {
|
||||||
|
const tree = parser.parse(content);
|
||||||
|
|
||||||
|
console.log('\n=== 语法树结构 ===');
|
||||||
|
let hasError = false;
|
||||||
|
tree.iterate({
|
||||||
|
enter: (node) => {
|
||||||
|
const depth = getDepth(node.node);
|
||||||
|
const indent = ' '.repeat(depth);
|
||||||
|
const text = content.slice(node.from, node.to);
|
||||||
|
const preview = text.length > 60 ? text.slice(0, 60) + '...' : text;
|
||||||
|
|
||||||
|
if (node.name === '⚠') {
|
||||||
|
hasError = true;
|
||||||
|
console.log(`${indent}⚠ 错误节点 [${node.from}-${node.to}]: ${JSON.stringify(preview)}`);
|
||||||
|
} else {
|
||||||
|
console.log(`${indent}${node.name.padEnd(30 - depth * 2)} [${String(node.from).padStart(3)}-${String(node.to).padEnd(3)}]: ${JSON.stringify(preview)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (expectError) {
|
||||||
|
expect(hasError).toBe(true);
|
||||||
|
} else {
|
||||||
|
expect(hasError).toBe(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDepth(currentNode: any): number {
|
||||||
|
let depth = 0;
|
||||||
|
let node = currentNode;
|
||||||
|
while (node && node.parent) {
|
||||||
|
depth++;
|
||||||
|
node = node.parent;
|
||||||
|
}
|
||||||
|
return depth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('✅ @xml - 正确使用固定 key', () => {
|
||||||
|
it('应该接受正确的 xml key', () => {
|
||||||
|
const content = `POST "https://api.example.com/soap" {
|
||||||
|
@xml {
|
||||||
|
xml: "<user><name>张三</name></user>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该拒绝错误的 key 名称', () => {
|
||||||
|
const content = `POST "https://api.example.com/soap" {
|
||||||
|
@xml {
|
||||||
|
data: "<user><name>张三</name></user>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容(应该有错误):');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该拒绝多个属性', () => {
|
||||||
|
const content = `POST "https://api.example.com/soap" {
|
||||||
|
@xml {
|
||||||
|
xml: "<user></user>",
|
||||||
|
other: "value"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容(应该有错误):');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('✅ @html - 正确使用固定 key', () => {
|
||||||
|
it('应该接受正确的 html key', () => {
|
||||||
|
const content = `POST "https://api.example.com/render" {
|
||||||
|
@html {
|
||||||
|
html: "<div><h1>标题</h1></div>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该拒绝错误的 key 名称', () => {
|
||||||
|
const content = `POST "https://api.example.com/render" {
|
||||||
|
@html {
|
||||||
|
content: "<div><h1>标题</h1></div>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容(应该有错误):');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('✅ @javascript - 正确使用固定 key', () => {
|
||||||
|
it('应该接受正确的 javascript key', () => {
|
||||||
|
const content = `POST "https://api.example.com/execute" {
|
||||||
|
@javascript {
|
||||||
|
javascript: "function hello() { return 'world'; }"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该拒绝错误的 key 名称', () => {
|
||||||
|
const content = `POST "https://api.example.com/execute" {
|
||||||
|
@javascript {
|
||||||
|
code: "function hello() { return 'world'; }"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容(应该有错误):');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('✅ @binary - 正确使用固定 key', () => {
|
||||||
|
it('应该接受正确的 binary key', () => {
|
||||||
|
const content = `POST "https://api.example.com/upload" {
|
||||||
|
@binary {
|
||||||
|
binary: "@file E://Documents/avatar.png"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该拒绝错误的 key 名称', () => {
|
||||||
|
const content = `POST "https://api.example.com/upload" {
|
||||||
|
@binary {
|
||||||
|
file: "@file E://Documents/avatar.png"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容(应该有错误):');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('✅ 对比:@json 和 @params 允许任意 key', () => {
|
||||||
|
it('@json 可以使用任意 key 名称', () => {
|
||||||
|
const content = `POST "https://api.example.com/api" {
|
||||||
|
@json {
|
||||||
|
name: "张三",
|
||||||
|
age: 25,
|
||||||
|
email: "test@example.com"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@params 可以使用任意 key 名称', () => {
|
||||||
|
const content = `GET "https://api.example.com/users" {
|
||||||
|
@params {
|
||||||
|
page: 1,
|
||||||
|
size: 20,
|
||||||
|
filter: "active"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('✅ 空块测试 - 现在支持空块', () => {
|
||||||
|
it('@xml 空块应该成功', () => {
|
||||||
|
const content = `POST "https://api.example.com/soap" {
|
||||||
|
@xml {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@html 空块应该成功', () => {
|
||||||
|
const content = `POST "https://api.example.com/render" {
|
||||||
|
@html {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@javascript 空块应该成功', () => {
|
||||||
|
const content = `POST "https://api.example.com/execute" {
|
||||||
|
@javascript {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@binary 空块应该成功', () => {
|
||||||
|
const content = `POST "https://api.example.com/upload" {
|
||||||
|
@binary {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('❌ 定义了 key 但没有值应该报错', () => {
|
||||||
|
it('@xml 定义了 xml key 但没有值', () => {
|
||||||
|
const content = `POST "https://api.example.com/soap" {
|
||||||
|
@xml {
|
||||||
|
xml:
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容(应该有错误):');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@html 定义了 html key 但没有值', () => {
|
||||||
|
const content = `POST "https://api.example.com/render" {
|
||||||
|
@html {
|
||||||
|
html:
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容(应该有错误):');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@javascript 定义了 javascript key 但没有值', () => {
|
||||||
|
const content = `POST "https://api.example.com/execute" {
|
||||||
|
@javascript {
|
||||||
|
javascript:
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容(应该有错误):');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@binary 定义了 binary key 但没有值', () => {
|
||||||
|
const content = `POST "https://api.example.com/upload" {
|
||||||
|
@binary {
|
||||||
|
binary:
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容(应该有错误):');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { parser } from './http.parser';
|
||||||
|
|
||||||
|
describe('HTTP Grammar - 新增请求格式测试', () => {
|
||||||
|
|
||||||
|
function parseAndCheck(content: string, expectError: boolean = false) {
|
||||||
|
const tree = parser.parse(content);
|
||||||
|
|
||||||
|
console.log('\n=== 语法树结构 ===');
|
||||||
|
let hasError = false;
|
||||||
|
tree.iterate({
|
||||||
|
enter: (node) => {
|
||||||
|
const depth = getDepth(node.node);
|
||||||
|
const indent = ' '.repeat(depth);
|
||||||
|
const text = content.slice(node.from, node.to);
|
||||||
|
const preview = text.length > 60 ? text.slice(0, 60) + '...' : text;
|
||||||
|
|
||||||
|
if (node.name === '⚠') {
|
||||||
|
hasError = true;
|
||||||
|
console.log(`${indent}⚠ 错误节点 [${node.from}-${node.to}]: ${JSON.stringify(preview)}`);
|
||||||
|
} else {
|
||||||
|
console.log(`${indent}${node.name.padEnd(30 - depth * 2)} [${String(node.from).padStart(3)}-${String(node.to).padEnd(3)}]: ${JSON.stringify(preview)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (expectError) {
|
||||||
|
expect(hasError).toBe(true);
|
||||||
|
} else {
|
||||||
|
expect(hasError).toBe(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDepth(currentNode: any): number {
|
||||||
|
let depth = 0;
|
||||||
|
let node = currentNode;
|
||||||
|
while (node && node.parent) {
|
||||||
|
depth++;
|
||||||
|
node = node.parent;
|
||||||
|
}
|
||||||
|
return depth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('✅ @params - URL 参数', () => {
|
||||||
|
const content = `GET "https://api.example.com/users" {
|
||||||
|
@params {
|
||||||
|
page: 1,
|
||||||
|
size: 20,
|
||||||
|
keyword: "张三"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @xml - XML 格式请求体', () => {
|
||||||
|
const content = `POST "https://api.example.com/soap" {
|
||||||
|
content-type: "application/xml"
|
||||||
|
|
||||||
|
@xml {
|
||||||
|
xml: "<user><name>张三</name><age>25</age></user>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @html - HTML 格式请求体', () => {
|
||||||
|
const content = `POST "https://api.example.com/render" {
|
||||||
|
content-type: "text/html"
|
||||||
|
|
||||||
|
@html {
|
||||||
|
html: "<div><h1>标题</h1><p>内容</p></div>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @javascript - JavaScript 格式请求体', () => {
|
||||||
|
const content = `POST "https://api.example.com/execute" {
|
||||||
|
content-type: "application/javascript"
|
||||||
|
|
||||||
|
@javascript {
|
||||||
|
javascript: "function hello() { return 'Hello World'; }"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @binary - 二进制文件上传', () => {
|
||||||
|
const content = `POST "https://api.example.com/upload" {
|
||||||
|
content-type: "application/octet-stream"
|
||||||
|
|
||||||
|
@binary {
|
||||||
|
binary: "@file E://Documents/avatar.png"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ 混合使用 - @params 和响应', () => {
|
||||||
|
const content = `GET "https://api.example.com/search" {
|
||||||
|
authorization: "Bearer token123"
|
||||||
|
|
||||||
|
@params {
|
||||||
|
q: "关键词",
|
||||||
|
page: 1,
|
||||||
|
limit: 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@response 200-OK 156ms 2025-11-03T10:30:00 {
|
||||||
|
"total": 100,
|
||||||
|
"data": []
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ 复杂 XML 内容', () => {
|
||||||
|
const content = `POST "https://api.example.com/soap" {
|
||||||
|
@xml {
|
||||||
|
xml: "<soap:Envelope xmlns:soap=\\"http://www.w3.org/2003/05/soap-envelope\\"><soap:Body><GetUser><UserId>123</UserId></GetUser></soap:Body></soap:Envelope>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ 多行 JavaScript', () => {
|
||||||
|
const content = `POST "https://api.example.com/run" {
|
||||||
|
@javascript {
|
||||||
|
javascript: "function calculate(a, b) {\\n return a + b;\\n}"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @binary 支持不同路径格式', () => {
|
||||||
|
const content = `POST "https://api.example.com/upload" {
|
||||||
|
@binary {
|
||||||
|
binary: "@file C:/Users/Documents/file.pdf"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @params 支持空值', () => {
|
||||||
|
const content = `GET "https://api.example.com/list" {
|
||||||
|
@params {
|
||||||
|
filter: "",
|
||||||
|
page: 1
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @xml 空块', () => {
|
||||||
|
const content = `POST "https://api.example.com/soap" {
|
||||||
|
content-type: "application/xml"
|
||||||
|
|
||||||
|
@xml {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @html 空块', () => {
|
||||||
|
const content = `POST "https://api.example.com/render" {
|
||||||
|
@html {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @javascript 空块', () => {
|
||||||
|
const content = `POST "https://api.example.com/execute" {
|
||||||
|
@javascript {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @binary 空块', () => {
|
||||||
|
const content = `POST "https://api.example.com/upload" {
|
||||||
|
@binary {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('❌ @xml 定义了 key 但没有值应该报错', () => {
|
||||||
|
const content = `POST "https://api.example.com/soap" {
|
||||||
|
@xml {
|
||||||
|
xml:
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('❌ @html 定义了 key 但没有值应该报错', () => {
|
||||||
|
const content = `POST "https://api.example.com/render" {
|
||||||
|
@html {
|
||||||
|
html:
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @xml 与其他格式混合', () => {
|
||||||
|
const content = `POST "https://api.example.com/multi" {
|
||||||
|
authorization: "Bearer token123"
|
||||||
|
|
||||||
|
@xml {
|
||||||
|
xml: "<data><item>test</item></data>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ 多个请求使用不同格式', () => {
|
||||||
|
const content = `POST "https://api.example.com/xml" {
|
||||||
|
@xml {
|
||||||
|
xml: "<user><name>张三</name></user>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POST "https://api.example.com/html" {
|
||||||
|
@html {
|
||||||
|
html: "<div>内容</div>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POST "https://api.example.com/js" {
|
||||||
|
@javascript {
|
||||||
|
javascript: "console.log('test');"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POST "https://api.example.com/binary" {
|
||||||
|
@binary {
|
||||||
|
binary: "@file C:/test.bin"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log('\n测试内容:');
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
parseAndCheck(content);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ describe('HTTP Grammar - @response 响应语法', () => {
|
|||||||
|
|
||||||
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
|
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
|
||||||
expect(hasNode(state, 'ErrorStatus')).toBe(true);
|
expect(hasNode(state, 'ErrorStatus')).toBe(true);
|
||||||
expect(hasNode(state, 'TimeUnit')).toBe(true);
|
expect(hasNode(state, 'TimeValue')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('✅ 响应与请求结合', () => {
|
it('✅ 响应与请求结合', () => {
|
||||||
@@ -145,8 +145,8 @@ POST "https://api.example.com/users" {
|
|||||||
|
|
||||||
const state = createTestState(content);
|
const state = createTestState(content);
|
||||||
|
|
||||||
expect(hasNode(state, 'TimeUnit')).toBe(true);
|
expect(hasNode(state, 'TimeValue')).toBe(true);
|
||||||
expect(getNodeText(state, 'TimeUnit')).toBe('ms');
|
expect(getNodeText(state, 'TimeValue')).toBe('12345ms');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('✅ 响应块包含复杂 JSON', () => {
|
it('✅ 响应块包含复杂 JSON', () => {
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ describe('HTTP Grammar 解析测试', () => {
|
|||||||
return depth;
|
return depth;
|
||||||
}
|
}
|
||||||
|
|
||||||
it('应该正确解析标准的 GET 请求(包含 @json 和 @res)', () => {
|
it('应该正确解析标准的 GET 请求(包含 @json)', () => {
|
||||||
const code = `GET "http://127.0.0.1:80/api/create" {
|
const code = `GET "http://127.0.0.1:80/api/create" {
|
||||||
host: "https://api.example.com",
|
host: "https://api.example.com",
|
||||||
content-type: "application/json",
|
content-type: "application/json",
|
||||||
@@ -97,17 +97,6 @@ describe('HTTP Grammar 解析测试', () => {
|
|||||||
@json {
|
@json {
|
||||||
name : "xxx",
|
name : "xxx",
|
||||||
test: "xx"
|
test: "xx"
|
||||||
}
|
|
||||||
|
|
||||||
@res {
|
|
||||||
code: 200,
|
|
||||||
status: "ok",
|
|
||||||
size: "20kb",
|
|
||||||
time: "2025-10-31 10:30:26",
|
|
||||||
data: {
|
|
||||||
xxx:"xxx"
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
@@ -277,7 +266,7 @@ POST "http://test2.com" {
|
|||||||
expect(result.hasError).toBe(false);
|
expect(result.hasError).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应该支持 @json/@res 块后面不加逗号(JSON块内部必须用逗号)', () => {
|
it('应该支持 @json 块后面不加逗号(JSON块内部必须用逗号)', () => {
|
||||||
const code = `POST "http://test.com" {
|
const code = `POST "http://test.com" {
|
||||||
host: "test.com"
|
host: "test.com"
|
||||||
|
|
||||||
@@ -285,11 +274,6 @@ POST "http://test2.com" {
|
|||||||
name: "xxx",
|
name: "xxx",
|
||||||
test: "xx"
|
test: "xx"
|
||||||
}
|
}
|
||||||
|
|
||||||
@res {
|
|
||||||
code: 200,
|
|
||||||
status: "ok"
|
|
||||||
}
|
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const tree = parseCode(code);
|
const tree = parseCode(code);
|
||||||
@@ -475,14 +459,6 @@ describe('HTTP 请求体格式测试', () => {
|
|||||||
level: "advanced"
|
level: "advanced"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@res {
|
|
||||||
code: 200,
|
|
||||||
message: "success",
|
|
||||||
data: {
|
|
||||||
id: 12345
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const tree = parseCode(code);
|
const tree = parseCode(code);
|
||||||
@@ -509,12 +485,6 @@ describe('HTTP 请求体格式测试', () => {
|
|||||||
age: 25,
|
age: 25,
|
||||||
description: "用户头像上传"
|
description: "用户头像上传"
|
||||||
}
|
}
|
||||||
|
|
||||||
@res {
|
|
||||||
code: 200,
|
|
||||||
message: "上传成功",
|
|
||||||
url: "https://cdn.example.com/avatar.png"
|
|
||||||
}
|
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const tree = parseCode(code);
|
const tree = parseCode(code);
|
||||||
@@ -539,12 +509,6 @@ describe('HTTP 请求体格式测试', () => {
|
|||||||
password: "123456",
|
password: "123456",
|
||||||
remember: true
|
remember: true
|
||||||
}
|
}
|
||||||
|
|
||||||
@res {
|
|
||||||
code: 200,
|
|
||||||
message: "登录成功",
|
|
||||||
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
|
|
||||||
}
|
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const tree = parseCode(code);
|
const tree = parseCode(code);
|
||||||
@@ -593,13 +557,6 @@ POST "http://api.example.com/login" {
|
|||||||
username: "admin",
|
username: "admin",
|
||||||
password: "123456"
|
password: "123456"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 期望的响应
|
|
||||||
@res {
|
|
||||||
code: 200,
|
|
||||||
# 用户token
|
|
||||||
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
|
|
||||||
}
|
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const tree = parseCode(code);
|
const tree = parseCode(code);
|
||||||
@@ -615,7 +572,7 @@ POST "http://api.example.com/login" {
|
|||||||
expect(result.hasError).toBe(false);
|
expect(result.hasError).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('✅ 混合多种格式 - JSON + 响应', () => {
|
it('✅ 混合多种格式 - JSON 请求', () => {
|
||||||
const code = `POST "http://api.example.com/login" {
|
const code = `POST "http://api.example.com/login" {
|
||||||
content-type: "application/json"
|
content-type: "application/json"
|
||||||
user-agent: "Mozilla/5.0"
|
user-agent: "Mozilla/5.0"
|
||||||
@@ -624,16 +581,6 @@ POST "http://api.example.com/login" {
|
|||||||
username: "admin",
|
username: "admin",
|
||||||
password: "123456"
|
password: "123456"
|
||||||
}
|
}
|
||||||
|
|
||||||
@res {
|
|
||||||
code: 200,
|
|
||||||
message: "登录成功",
|
|
||||||
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
|
|
||||||
user: {
|
|
||||||
id: 1,
|
|
||||||
name: "管理员"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const tree = parseCode(code);
|
const tree = parseCode(code);
|
||||||
@@ -723,3 +670,442 @@ POST "http://api.example.com/login" {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('HTTP 新格式测试 - params/xml/html/javascript/binary', () => {
|
||||||
|
|
||||||
|
function parseCode(code: string) {
|
||||||
|
const tree = parser.parse(code);
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasErrorNodes(tree: any): { hasError: boolean; errors: Array<{ name: string; from: number; to: number; text: string }> } {
|
||||||
|
const errors: Array<{ name: string; from: number; to: number; text: string }> = [];
|
||||||
|
|
||||||
|
tree.iterate({
|
||||||
|
enter: (node: any) => {
|
||||||
|
if (node.name === '⚠') {
|
||||||
|
errors.push({
|
||||||
|
name: node.name,
|
||||||
|
from: node.from,
|
||||||
|
to: node.to,
|
||||||
|
text: tree.toString().substring(node.from, node.to)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasError: errors.length > 0,
|
||||||
|
errors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
it('✅ @params - URL 参数格式', () => {
|
||||||
|
const code = `GET "http://api.example.com/users" {
|
||||||
|
authorization: "Bearer token123"
|
||||||
|
|
||||||
|
@params {
|
||||||
|
page: 1,
|
||||||
|
size: 20,
|
||||||
|
keyword: "张三",
|
||||||
|
status: "active"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ @params 格式错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @xml - XML 格式', () => {
|
||||||
|
const code = `POST "http://api.example.com/soap" {
|
||||||
|
content-type: "application/xml"
|
||||||
|
|
||||||
|
@xml {
|
||||||
|
xml: "<user><name>张三</name><age>25</age></user>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ @xml 格式错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @xml - 空块', () => {
|
||||||
|
const code = `POST "http://api.example.com/soap" {
|
||||||
|
@xml {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ @xml 空块格式错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @html - HTML 格式', () => {
|
||||||
|
const code = `POST "http://api.example.com/render" {
|
||||||
|
content-type: "text/html"
|
||||||
|
|
||||||
|
@html {
|
||||||
|
html: "<div><h1>标题</h1><p>内容</p></div>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ @html 格式错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @html - 空块', () => {
|
||||||
|
const code = `POST "http://api.example.com/render" {
|
||||||
|
@html {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ @html 空块格式错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @javascript - JavaScript 格式', () => {
|
||||||
|
const code = `POST "http://api.example.com/execute" {
|
||||||
|
content-type: "application/javascript"
|
||||||
|
|
||||||
|
@javascript {
|
||||||
|
javascript: "function hello() { return 'Hello World'; }"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ @javascript 格式错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @javascript - 空块', () => {
|
||||||
|
const code = `POST "http://api.example.com/execute" {
|
||||||
|
@javascript {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ @javascript 空块格式错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @binary - 二进制文件上传', () => {
|
||||||
|
const code = `POST "http://api.example.com/upload" {
|
||||||
|
content-type: "application/octet-stream"
|
||||||
|
|
||||||
|
@binary {
|
||||||
|
binary: "@file E://Documents/avatar.png"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ @binary 格式错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ @binary - 空块', () => {
|
||||||
|
const code = `POST "http://api.example.com/upload" {
|
||||||
|
@binary {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ @binary 空块格式错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ 复杂 XML - SOAP 请求', () => {
|
||||||
|
const code = `POST "http://api.example.com/soap" {
|
||||||
|
@xml {
|
||||||
|
xml: "<soap:Envelope xmlns:soap=\\"http://www.w3.org/2003/05/soap-envelope\\"><soap:Body><GetUser><UserId>123</UserId></GetUser></soap:Body></soap:Envelope>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ 复杂 XML 格式错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ 混合使用 - params + headers', () => {
|
||||||
|
const code = `GET "http://api.example.com/search" {
|
||||||
|
authorization: "Bearer token123"
|
||||||
|
user-agent: "Mozilla/5.0"
|
||||||
|
|
||||||
|
@params {
|
||||||
|
q: "搜索关键词",
|
||||||
|
page: 1,
|
||||||
|
limit: 50,
|
||||||
|
sort: "desc"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ 混合使用格式错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ 多个不同新格式的请求', () => {
|
||||||
|
const code = `# XML 请求
|
||||||
|
POST "http://api.example.com/xml" {
|
||||||
|
@xml {
|
||||||
|
xml: "<user><name>张三</name></user>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTML 请求
|
||||||
|
POST "http://api.example.com/html" {
|
||||||
|
@html {
|
||||||
|
html: "<div>内容</div>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# JavaScript 请求
|
||||||
|
POST "http://api.example.com/js" {
|
||||||
|
@javascript {
|
||||||
|
javascript: "console.log('test');"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Binary 请求
|
||||||
|
POST "http://api.example.com/upload" {
|
||||||
|
@binary {
|
||||||
|
binary: "@file C:/test.bin"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ 多新格式请求错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
|
||||||
|
// 统计 RequestStatement 数量
|
||||||
|
let requestCount = 0;
|
||||||
|
tree.iterate({
|
||||||
|
enter: (node: any) => {
|
||||||
|
if (node.name === 'RequestStatement') requestCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(requestCount).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('❌ @xml - 定义了 xml key 但没有值(应该报错)', () => {
|
||||||
|
const code = `POST "http://api.example.com/soap" {
|
||||||
|
@xml {
|
||||||
|
xml:
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
// 应该有错误
|
||||||
|
expect(result.hasError).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('❌ @html - 定义了 html key 但没有值(应该报错)', () => {
|
||||||
|
const code = `POST "http://api.example.com/render" {
|
||||||
|
@html {
|
||||||
|
html:
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
// 应该有错误
|
||||||
|
expect(result.hasError).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('❌ @xml - 使用错误的 key 名称(应该报错)', () => {
|
||||||
|
const code = `POST "http://api.example.com/soap" {
|
||||||
|
@xml {
|
||||||
|
data: "<user><name>张三</name></user>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
// 应该有错误
|
||||||
|
expect(result.hasError).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('✅ 所有格式组合测试', () => {
|
||||||
|
const code = `# 传统格式
|
||||||
|
POST "http://api.example.com/json" {
|
||||||
|
@json {
|
||||||
|
name: "test",
|
||||||
|
age: 25
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POST "http://api.example.com/form" {
|
||||||
|
@formdata {
|
||||||
|
file: "test.png",
|
||||||
|
desc: "description"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POST "http://api.example.com/login" {
|
||||||
|
@urlencoded {
|
||||||
|
username: "admin",
|
||||||
|
password: "123456"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POST "http://api.example.com/text" {
|
||||||
|
@text {
|
||||||
|
content: "纯文本内容"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 新格式
|
||||||
|
GET "http://api.example.com/search" {
|
||||||
|
@params {
|
||||||
|
q: "keyword",
|
||||||
|
page: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POST "http://api.example.com/xml" {
|
||||||
|
@xml {
|
||||||
|
xml: "<data>test</data>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POST "http://api.example.com/html" {
|
||||||
|
@html {
|
||||||
|
html: "<div>test</div>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POST "http://api.example.com/js" {
|
||||||
|
@javascript {
|
||||||
|
javascript: "alert('test');"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POST "http://api.example.com/upload" {
|
||||||
|
@binary {
|
||||||
|
binary: "@file C:/file.bin"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tree = parseCode(code);
|
||||||
|
const result = hasErrorNodes(tree);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
console.log('\n❌ 所有格式组合测试错误:');
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.hasError).toBe(false);
|
||||||
|
|
||||||
|
// 统计 RequestStatement 数量(应该有9个)
|
||||||
|
let requestCount = 0;
|
||||||
|
tree.iterate({
|
||||||
|
enter: (node: any) => {
|
||||||
|
if (node.name === 'RequestStatement') requestCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(requestCount).toBe(9);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,18 @@ export const httpHighlighting = styleTags({
|
|||||||
"TRACE CONNECT": t.modifier,
|
"TRACE CONNECT": t.modifier,
|
||||||
|
|
||||||
// ========== @ 规则(请求体格式和变量声明)==========
|
// ========== @ 规则(请求体格式和变量声明)==========
|
||||||
// @json, @formdata, @urlencoded - 使用类型名
|
// @json, @formdata, @urlencoded, @params - 使用类型名
|
||||||
"JsonKeyword FormDataKeyword UrlEncodedKeyword": t.typeName,
|
"JsonKeyword FormDataKeyword UrlEncodedKeyword ParamsKeyword": t.typeName,
|
||||||
|
|
||||||
// @text - 使用特殊类型
|
// @text - 使用特殊类型
|
||||||
"TextKeyword": t.special(t.typeName),
|
"TextKeyword": t.special(t.typeName),
|
||||||
|
|
||||||
|
// @xml, @html, @javascript - 使用类型名
|
||||||
|
"XmlKeyword HtmlKeyword JavaScriptKeyword": t.typeName,
|
||||||
|
|
||||||
|
// @binary - 使用特殊类型
|
||||||
|
"BinaryKeyword": t.special(t.typeName),
|
||||||
|
|
||||||
// @var - 变量声明关键字
|
// @var - 变量声明关键字
|
||||||
"VarKeyword": t.definitionKeyword,
|
"VarKeyword": t.definitionKeyword,
|
||||||
|
|
||||||
@@ -60,15 +66,16 @@ export const httpHighlighting = styleTags({
|
|||||||
|
|
||||||
// ========== 响应相关 ==========
|
// ========== 响应相关 ==========
|
||||||
// 响应状态码 - 数字颜色
|
// 响应状态码 - 数字颜色
|
||||||
"StatusCode": t.number,
|
"ResponseStatus/NumberLiteral": t.number,
|
||||||
"ResponseStatus/StatusCode": t.number,
|
"ResponseStatus/identifier": t.constant(t.variableName),
|
||||||
|
|
||||||
// 响应错误状态 - 关键字
|
// 响应错误状态 - 关键字
|
||||||
"ErrorStatus": t.operatorKeyword,
|
"ErrorStatus": t.operatorKeyword,
|
||||||
|
|
||||||
// 响应时间 - 数字颜色
|
// 响应时间 - 数字和单位颜色
|
||||||
"TimeValue": t.number,
|
"TimeValue": t.number,
|
||||||
"ResponseTime": t.number,
|
"ResponseTime/TimeValue": t.number,
|
||||||
|
"TimeUnit": t.unit,
|
||||||
|
|
||||||
// 时间戳 - 字符串颜色
|
// 时间戳 - 字符串颜色
|
||||||
"Timestamp": t.string,
|
"Timestamp": t.string,
|
||||||
@@ -99,6 +106,12 @@ export const httpHighlighting = styleTags({
|
|||||||
"JsonValue/StringLiteral": t.string,
|
"JsonValue/StringLiteral": t.string,
|
||||||
"JsonValue/NumberLiteral": t.number,
|
"JsonValue/NumberLiteral": t.number,
|
||||||
|
|
||||||
|
// ========== 固定 key 名称(xml、html、javascript、binary)==========
|
||||||
|
"XmlKey": t.constant(t.propertyName),
|
||||||
|
"HtmlKey": t.constant(t.propertyName),
|
||||||
|
"JavaScriptKey": t.constant(t.propertyName),
|
||||||
|
"BinaryKey": t.constant(t.propertyName),
|
||||||
|
|
||||||
// ========== 标点符号 ==========
|
// ========== 标点符号 ==========
|
||||||
// 冒号 - 分隔符
|
// 冒号 - 分隔符
|
||||||
":": t.separator,
|
":": t.separator,
|
||||||
|
|||||||
@@ -40,17 +40,34 @@ export const
|
|||||||
UrlEncodedKeyword = 44,
|
UrlEncodedKeyword = 44,
|
||||||
TextRule = 45,
|
TextRule = 45,
|
||||||
TextKeyword = 46,
|
TextKeyword = 46,
|
||||||
ResponseDeclaration = 47,
|
ParamsRule = 47,
|
||||||
ResponseKeyword = 48,
|
ParamsKeyword = 48,
|
||||||
ResponseStatus = 49,
|
XmlRule = 49,
|
||||||
StatusCode = 50,
|
XmlKeyword = 50,
|
||||||
ErrorStatus = 51,
|
XmlBlock = 51,
|
||||||
ResponseTime = 52,
|
XmlKey = 52,
|
||||||
TimeValue = 53,
|
HtmlRule = 53,
|
||||||
ResponseTimestamp = 54,
|
HtmlKeyword = 54,
|
||||||
Timestamp = 55,
|
HtmlBlock = 55,
|
||||||
ResponseBlock = 56,
|
HtmlKey = 56,
|
||||||
JsonObject = 57,
|
JavaScriptRule = 57,
|
||||||
JsonMember = 58,
|
JavaScriptKeyword = 58,
|
||||||
JsonValue = 59,
|
JavaScriptBlock = 59,
|
||||||
JsonArray = 62
|
JavaScriptKey = 60,
|
||||||
|
BinaryRule = 61,
|
||||||
|
BinaryKeyword = 62,
|
||||||
|
BinaryBlock = 63,
|
||||||
|
BinaryKey = 64,
|
||||||
|
ResponseDeclaration = 65,
|
||||||
|
ResponseKeyword = 66,
|
||||||
|
ResponseStatus = 67,
|
||||||
|
ErrorStatus = 68,
|
||||||
|
ResponseTime = 69,
|
||||||
|
TimeValue = 70,
|
||||||
|
ResponseTimestamp = 71,
|
||||||
|
Timestamp = 72,
|
||||||
|
ResponseBlock = 73,
|
||||||
|
JsonObject = 74,
|
||||||
|
JsonMember = 75,
|
||||||
|
JsonValue = 76,
|
||||||
|
JsonArray = 79
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||||
import {LRParser} from "@lezer/lr"
|
import {LRParser} from "@lezer/lr"
|
||||||
import {httpHighlighting} from "./http.highlight"
|
import {httpHighlighting} from "./http.highlight"
|
||||||
const spec_AtKeyword = {__proto__:null,"@var":10, "@json":80, "@formdata":84, "@urlencoded":88, "@text":92, "@response":96}
|
const spec_AtKeyword = {__proto__:null,"@var":10, "@json":80, "@formdata":84, "@urlencoded":88, "@text":92, "@params":96, "@xml":100, "@html":108, "@javascript":116, "@binary":124, "@response":132}
|
||||||
const spec_identifier = {__proto__:null,true:34, false:38, null:42, GET:50, POST:52, PUT:54, DELETE:56, PATCH:58, HEAD:60, OPTIONS:62, CONNECT:64, TRACE:66, error:102}
|
const spec_identifier = {__proto__:null,true:34, false:38, null:42, GET:50, POST:52, PUT:54, DELETE:56, PATCH:58, HEAD:60, OPTIONS:62, CONNECT:64, TRACE:66, xml:104, html:112, javascript:120, binary:128, error:136}
|
||||||
export const parser = LRParser.deserialize({
|
export const parser = LRParser.deserialize({
|
||||||
version: 14,
|
version: 14,
|
||||||
states: "-bQYQPOOO!aQPO'#C_OOQO'#Ct'#CtO!aQPO'#DTO!aQPO'#DVO!aQPO'#DXO!aQPO'#DZO!fQPO'#DSO#yQPO'#CsO$jQPO'#DlO$qQPO'#DgO$|QPO'#D]OOQO'#Du'#DuOOQO'#Dm'#DmQYQPOOO%UQPO'#CdOOQO,58y,58yOOQO,59o,59oOOQO,59q,59qOOQO,59s,59sOOQO,59u,59uOOQO,59n,59nOOQO'#DO'#DOO%^QPO,59_O%cQPO'#CiOOQO'#Cl'#ClOOQO'#Cn'#CnOOQO'#Cp'#CpO&QQPO'#D}OOQO,5:W,5:WO&YQPO,5:WOOQO'#Di'#DiO&_QPO'#DhO&dQPO'#D|OOQO,5:R,5:RO&lQPO,5:ROOQO'#D_'#D_O&qQPO,59wOOQO-E7k-E7kOOQO'#Cf'#CfO&vQPO'#CeO&{QPO'#DvOOQO,59O,59OO'TQPO,59OO'kQPO'#DPOOQO1G.y1G.yOOQO,59T,59TO'rQPO,5:iO'yQPO,5:iOOQO1G/r1G/rO$OQPO,5:SO(RQPO,5:hO(^QPO,5:hOOQO1G/m1G/mOOQO'#Db'#DbO(fQPO1G/cO(kQPO,59PO)SQPO,5:bO)[QPO,5:bOOQO1G.j1G.jOOQO'#DR'#DRO)dQPO'#DQOOQO'#Do'#DoO)iQPO'#DzO)pQPO'#DzOOQO,59k,59kO)xQPO,59kOOQO,5:[,5:[O)}QPO1G0TOOQO-E7n-E7nOOQO1G/n1G/nOOQO,5:],5:]O*UQPO1G0SOOQO-E7o-E7oOOQO'#Dd'#DdO*aQPO7+$}OOQO'#Dx'#DxOOQO1G.k1G.kOOQO,5:Y,5:YO*iQPO1G/|OOQO-E7l-E7lO*qQPO,59lOOQO-E7m-E7mO+SQPO,5:fO+SQPO,5:fOOQO1G/V1G/VP$OQPO'#DpP$tQPO'#DqOOQO'#Df'#DfOOQO<<Hi<<HiP%XQPO'#DnOOQO'#D{'#D{O+[QPO1G/WO+sQPO1G0QOOQO7+$r7+$r",
|
states: "2`QYQPOOO!pQPO'#C_OOQO'#Ct'#CtO!pQPO'#DTO!pQPO'#DVO!pQPO'#DXO!pQPO'#DZO!pQPO'#D]O!uQPO'#D_O!zQPO'#DcO#PQPO'#DgO#UQPO'#DkO#ZQPO'#DSO$}QPO'#CsO%nQPO'#D}O%uQPO'#DxO&QQPO'#DoOOQO'#EW'#EWOOQO'#EO'#EOQYQPOOO&YQPO'#CdOOQO,58y,58yOOQO,59o,59oOOQO,59q,59qOOQO,59s,59sOOQO,59u,59uOOQO,59w,59wO&bQPO'#DaOOQO,59y,59yO&jQPO'#DeOOQO,59},59}O&rQPO'#DiOOQO,5:R,5:RO&zQPO'#DmOOQO,5:V,5:VOOQO,59n,59nOOQO'#DO'#DOO'SQPO,59_O'XQPO'#CiOOQO'#Cl'#ClOOQO'#Cn'#CnOOQO'#Cp'#CpO(VQPO'#EaOOQO,5:i,5:iO(_QPO,5:iOOQO'#Dz'#DzO(dQPO'#DyO(iQPO'#E`OOQO,5:d,5:dO(qQPO,5:dO(vQQO'#CiO)RQQO'#DqOOQO'#Dq'#DqO)ZQPO,5:ZOOQO-E7|-E7|OOQO'#Cf'#CfO)`QPO'#CeO)eQPO'#EXOOQO,59O,59OO)mQPO,59OO)rQPO,59{OOQO,59{,59{O)wQPO,5:POOQO,5:P,5:PO)|QPO,5:TOOQO,5:T,5:TO*RQPO,5:XOOQO,5:X,5:XO*xQPO'#DPOOQO1G.y1G.yOOQO,59T,59TO+PQPO,5:{O+WQPO,5:{OOQO1G0T1G0TO%SQPO,5:eO+`QPO,5:zO+kQPO,5:zOOQO1G0O1G0OO+sQPO,5:]OOQO'#Ds'#DsO+xQPO1G/uO+}QPO,59PO,fQPO,5:sO,nQPO,5:sOOQO1G.j1G.jO+}QPO1G/gO+}QPO1G/kO+}QPO1G/oO+}QPO1G/sOOQO'#DR'#DRO,vQPO'#DQOOQO'#EQ'#EQO,{QPO'#E]O-SQPO'#E]OOQO,59k,59kO-[QPO,59kOOQO,5:m,5:mO-aQPO1G0gOOQO-E8P-E8POOQO1G0P1G0POOQO,5:n,5:nO-hQPO1G0fOOQO-E8Q-E8QOOQO1G/w1G/wOOQO'#Du'#DuO-sQPO7+%aOOQO'#EZ'#EZOOQO1G.k1G.kOOQO,5:k,5:kO-{QPO1G0_OOQO-E7}-E7}O.TQPO7+%RO.YQPO7+%VO._QPO7+%ZO.dQPO7+%_O.iQPO,59lOOQO-E8O-E8OO.zQPO,5:wO.zQPO,5:wOOQO1G/V1G/VP%SQPO'#ERP%xQPO'#ESOOQO'#Dw'#DwOOQO<<H{<<H{P&]QPO'#EPOOQO<<Hm<<HmOOQO<<Hq<<HqOOQO<<Hu<<HuOOQO<<Hy<<HyOOQO'#E^'#E^O/SQPO1G/WO/zQPO1G0cOOQO7+$r7+$r",
|
||||||
stateData: ",U~O!hOSPOS~OTPOVYOiQOjQOkQOlQOmQOnQOoQOpQOqQOxROzSO|TO!OUO!QZO!_XO~OV_O~OfeOTvXVvXivXjvXkvXlvXmvXnvXovXpvXqvXxvXzvX|vX!OvX!QvX!_vX!fvXUvX!kvX~O[fO~OVYO[oO_oOaiOcjOekO!_XO!mhO~O!^mO~P$OOUrO[pO!kpO~O!StO!TtO~OUzO!kwO~OV|O~O^!OOf]X!^]XU]Xx]Xz]X|]X!O]X!k]X~Of!PO!^!qX~O!^!RO~OZ!SO~Of!TOU!pX~OU!VO~O!V!WO~OZ!YO~Of!ZOU!jX~OU!]O~OxROzSO|TO!OUO!k!^O~OU!cO~P'YO!^!qa~P$OOf!fO!^!qa~O[pO!kpOU!pa~Of!jOU!pa~O!X!lO~OV_O[!nO_!nOaiOcjOekO!mhO~O!kwOU!ja~Of!qOU!ja~OZ!sO~OU!nX~P'YO!k!^OU!nX~OU!wO~O!^!qi~P$OO[pO!kpOU!pi~OVYO!_XO~O!kwOU!ji~OV|O[!}O_!}O!k!}O!mhO~O!k!^OU!na~Of#QOUtixtizti|ti!Oti!kti~O!k!^OU!ni~O!X!V!S!m_!k^!m~",
|
stateData: "0[~O!yOSPOS~OTPOV_OiQOjQOkQOlQOmQOnQOoQOpQOqQOxROzSO|TO!OUO!QVO!SWO!WXO![YO!`ZO!d`O!p^O~OVdO~OVkO~OVmO~OVoO~OVqO~OfsOTvXVvXivXjvXkvXlvXmvXnvXovXpvXqvXxvXzvX|vX!OvX!QvX!SvX!WvX![vX!`vX!dvX!pvX!wvXUvX!|vX~O[tO~OV_O[}O_}OawOcxOeyO!p^O#OvO~O!o{O~P%SOU!QO[!OO!|!OO~O!f!UO#O!SO~OU![O!|!XO~OU!_O!U!^O~OU!aO!Y!`O~OU!cO!^!bO~OU!eO!b!dO~OV!fO~O^!hOf]X!o]XU]Xx]Xz]X|]X!O]X!Q]X!S]X!W]X![]X!`]X!|]X~Of!iO!o#TX~O!o!kO~OZ!lO~Of!mOU#SX~OU!oO~O^!hO!h]X#R]X~O#R!pO!h!eX~O!h!qO~OZ!sO~Of!tOU!{X~OU!vO~OZ!wO~OZ!xO~OZ!yO~OZ!zO~OxROzSO|TO!OUO!QVO!SWO!WXO![YO!`ZO!|!{O~OU#QO~P*WO!o#Ta~P%SOf#TO!o#Ta~O[!OO!|!OOU#Sa~Of#XOU#Sa~O!|#ZO~O!j#[O~OVdO[#^O_#^OawOcxOeyO#OvO~O!|!XOU!{a~Of#aOU!{a~OZ#gO~OU#PX~P*WO!|!{OU#PX~OU#kO~O!o#Ti~P%SO[!OO!|!OOU#Si~OV_O!p^O~O!|!XOU!{i~OU#qO~OU#rO~OU#sO~OU#tO~OV!fO[#uO_#uO!|#uO#OvO~O!|!{OU#Pa~Of#xOUtixtizti|ti!Oti!Qti!Sti!Wti![ti!`ti!|ti~O!|!{OU#Pi~O!j!h#O_!|^_~",
|
||||||
goto: "'^!rPPP!sPPPP!w#Z#cPP#iPP#vP#vP#vPP!s$QPPPPPPPPP$U$X$_$g$o$yP$yP$yP$yP!sP%PPP%SP%VP%Y%]%k%sPP%]&O&U&[&j&pPPP&v&zP&}P'Q'T'W'ZT[O^Q`PQaRQbSQcTQdUR!n!YQy_V!p!Z!q!|Xx_!Z!q!|YoX!P!S!f!xQ!n!YR!}!sYoX!P!S!f!xR!n!YTWO^RgWQ}gR!}!s]!`|!a!b!u!v#P]!_|!a!b!u!v#PS[O^Q!b|R!u!aXVO^|!aRuZR!XuR!m!XR!{!mS[O^YoX!P!S!f!xR!z!mQqYV!i!T!j!yQlXU!e!P!f!xR!h!SQ^ORv^Q![yR!r![Q!a|U!t!a!v#PQ!v!bR#P!uQ!QlR!g!QQ!UqR!k!UT]O^R{_R!o!YR!d|R#O!sRsYRnX",
|
goto: "(l#UPPP#VPPPP#Z#t#|PP$SPP$hP$hP$hPP#V$vPPPPPPPPP$z$}%T%]%e%oP%oP%oP%oP%oP%oP%uP%oP%xP%oP%{P%oP&OP#VP&RP&UP&XP&[&_&m&uPP&_'Q'W'^'l'rPPP'x'|P(PP(`(cP(f(iTaOcQePQfRQgSQhTQiUQjVZ#^!s!w!x!y!zQ!ZdV#`!t#a#pX!Yd!t#a#pY}^!i!l#T#lQ!T`Y#^!s!w!x!y!zR#u#gY}^!i!l#T#lZ#^!s!w!x!y!zT]OcRu]Q!guR#u#g]!}!f#O#P#i#j#w]!|!f#O#P#i#j#wSaOcQ#P!fR#i#OX[Oc!f#ORlWRnXRpYRrZR!V`R!r!VR#]!rR#o#]SaOcY}^!i!l#T#lR#n#]Q!P_V#W!m#X#mQz^U#S!i#T#lR#V!lQcOR!WcQ!u!ZR#b!uQ#O!fU#h#O#j#wQ#j#PR#w#iQ!jzR#U!jQ!n!PR#Y!nTbOcR!]dQ#_!sQ#c!wQ#d!xQ#e!yR#f!zR#R!fR#v#gR!R_R|^",
|
||||||
nodeNames: "⚠ LineComment Document VarDeclaration AtKeyword VarKeyword } { JsonBlock JsonProperty PropertyName : StringLiteral NumberLiteral Unit VariableRef JsonTrue True JsonFalse False JsonNull Null , RequestStatement Method GET POST PUT DELETE PATCH HEAD OPTIONS CONNECT TRACE Url Block Property PropertyName AtRule JsonRule JsonKeyword FormDataRule FormDataKeyword UrlEncodedRule UrlEncodedKeyword TextRule TextKeyword ResponseDeclaration ResponseKeyword ResponseStatus StatusCode ErrorStatus ResponseTime TimeValue ResponseTimestamp Timestamp ResponseBlock JsonObject JsonMember JsonValue ] [ JsonArray",
|
nodeNames: "⚠ LineComment Document VarDeclaration AtKeyword VarKeyword } { JsonBlock JsonProperty PropertyName : StringLiteral NumberLiteral Unit VariableRef JsonTrue True JsonFalse False JsonNull Null , RequestStatement Method GET POST PUT DELETE PATCH HEAD OPTIONS CONNECT TRACE Url Block Property PropertyName AtRule JsonRule JsonKeyword FormDataRule FormDataKeyword UrlEncodedRule UrlEncodedKeyword TextRule TextKeyword ParamsRule ParamsKeyword XmlRule XmlKeyword XmlBlock XmlKey HtmlRule HtmlKeyword HtmlBlock HtmlKey JavaScriptRule JavaScriptKeyword JavaScriptBlock JavaScriptKey BinaryRule BinaryKeyword BinaryBlock BinaryKey ResponseDeclaration ResponseKeyword ResponseStatus ErrorStatus ResponseTime TimeValue ResponseTimestamp Timestamp ResponseBlock JsonObject JsonMember JsonValue ] [ JsonArray",
|
||||||
maxTerm: 79,
|
maxTerm: 97,
|
||||||
nodeProps: [
|
nodeProps: [
|
||||||
["openedBy", 6,"{",60,"["],
|
["openedBy", 6,"{",77,"["],
|
||||||
["closedBy", 7,"}",61,"]"],
|
["closedBy", 7,"}",78,"]"],
|
||||||
["isolate", -3,12,15,55,""]
|
["isolate", -4,12,15,70,72,""]
|
||||||
],
|
],
|
||||||
propSources: [httpHighlighting],
|
propSources: [httpHighlighting],
|
||||||
skippedNodes: [0,1,4],
|
skippedNodes: [0,1,4],
|
||||||
repeatNodeCount: 5,
|
repeatNodeCount: 5,
|
||||||
tokenData: "3b~RlX^!ypq!yrs#nst%btu%ywx&b{|(P|})k}!O(P!O!P(Y!Q![)p![!]/Y!b!c/_!c!}0V!}#O0p#P#Q0u#R#S%y#T#o0V#o#p0z#q#r3]#y#z!y$f$g!y#BY#BZ!y$IS$I_!y$I|$JO!y$JT$JU!y$KV$KW!y&FU&FV!y~#OY!h~X^!ypq!y#y#z!y$f$g!y#BY#BZ!y$IS$I_!y$I|$JO!y$JT$JU!y$KV$KW!y&FU&FV!y~#qWOY#nZr#nrs$Zs#O#n#O#P$`#P;'S#n;'S;=`%[<%lO#n~$`O[~~$cRO;'S#n;'S;=`$l;=`O#n~$oXOY#nZr#nrs$Zs#O#n#O#P$`#P;'S#n;'S;=`%[;=`<%l#n<%lO#n~%_P;=`<%l#n~%gSP~OY%bZ;'S%b;'S;=`%s<%lO%b~%vP;=`<%l%b~&OU!k~tu%y}!O%y!Q![%y!c!}%y#R#S%y#T#o%y~&eWOY&bZw&bwx$Zx#O&b#O#P&}#P;'S&b;'S;=`'y<%lO&b~'QRO;'S&b;'S;=`'Z;=`O&b~'^XOY&bZw&bwx$Zx#O&b#O#P&}#P;'S&b;'S;=`'y;=`<%l&b<%lO&b~'|P;=`<%l&b~(SQ!O!P(Y!Q![)Y~(]P!Q![(`~(eR!m~!Q![(`!g!h(n#X#Y(n~(qR{|(z}!O(z!Q![)Q~(}P!Q![)Q~)VP!m~!Q![)Q~)_S!m~!O!P(`!Q![)Y!g!h(n#X#Y(n~)pOf~~)wU!S~!m~}!O*Z!O!P(`!Q![*r!g!h(n#X#Y(n#a#b.}~*^Q!c!}*d#T#o*d~*iR!S~}!O*d!c!}*d#T#o*d~*yU!S~!m~}!O*Z!O!P(`!Q![+]!g!h(n#X#Y(n#a#b.}~+dU!S~!m~}!O*Z!O!P(`!Q![+v!g!h(n#X#Y(n#a#b.}~+}U!S~!m~}!O,a!O!P(`!Q![.d!g!h(n#X#Y(n#a#b.}~,dR!Q![,m!c!}*d#T#o*d~,pP!Q![,s~,vP}!O,y~,|P!Q![-P~-SP!Q![-V~-YP!v!w-]~-`P!Q![-c~-fP!Q![-i~-lP![!]-o~-rP!Q![-u~-xP!Q![-{~.OP![!].R~.UP!Q![.X~.[P!Q![._~.dO!X~~.kU!S~!m~}!O*Z!O!P(`!Q![.d!g!h(n#X#Y(n#a#b.}~/QP#g#h/T~/YO!V~~/_OZ~~/bR}!O/k!c!}/t#T#o/t~/nQ!c!}/t#T#o/t~/ySS~}!O/t!Q![/t!c!}/t#T#o/t~0^U!k~^~tu%y}!O%y!Q![%y!c!}0V#R#S%y#T#o0V~0uO!_~~0zO!^~~1PPV~#o#p1S~1VStu1c!c!}1c#R#S1c#T#o1c~1fXtu1c}!O1c!O!P1c!Q![1c![!]2R!c!}1c#R#S1c#T#o1c#q#r3V~2UUOY2RZ#q2R#q#r2h#r;'S2R;'S;=`3P<%lO2R~2kTO#q2R#q#r2z#r;'S2R;'S;=`3P<%lO2R~3PO_~~3SP;=`<%l2R~3YP#q#r2z~3bOU~",
|
tokenData: "2h~RlX^!ypq!yrs#nst%btu%ywx&b{|(P|})k}!O)p!O!P(Y!Q![){![!].`!b!c.e!c!}/]!}#O/v#P#Q/{#R#S%y#T#o/]#o#p0Q#q#r2c#y#z!y$f$g!y#BY#BZ!y$IS$I_!y$I|$JO!y$JT$JU!y$KV$KW!y&FU&FV!y~#OY!y~X^!ypq!y#y#z!y$f$g!y#BY#BZ!y$IS$I_!y$I|$JO!y$JT$JU!y$KV$KW!y&FU&FV!y~#qWOY#nZr#nrs$Zs#O#n#O#P$`#P;'S#n;'S;=`%[<%lO#n~$`O[~~$cRO;'S#n;'S;=`$l;=`O#n~$oXOY#nZr#nrs$Zs#O#n#O#P$`#P;'S#n;'S;=`%[;=`<%l#n<%lO#n~%_P;=`<%l#n~%gSP~OY%bZ;'S%b;'S;=`%s<%lO%b~%vP;=`<%l%b~&OU!|~tu%y}!O%y!Q![%y!c!}%y#R#S%y#T#o%y~&eWOY&bZw&bwx$Zx#O&b#O#P&}#P;'S&b;'S;=`'y<%lO&b~'QRO;'S&b;'S;=`'Z;=`O&b~'^XOY&bZw&bwx$Zx#O&b#O#P&}#P;'S&b;'S;=`'y;=`<%l&b<%lO&b~'|P;=`<%l&bP(SQ!O!P(Y!Q![)YP(]P!Q![(`P(eR#OP!Q![(`!g!h(n#X#Y(nP(qR{|(z}!O(z!Q![)QP(}P!Q![)QP)VP#OP!Q![)QP)_S#OP!O!P(`!Q![)Y!g!h(n#X#Y(n~)pOf~R)uQ#RQ!O!P(Y!Q![)Y~*QT#OP!O!P(`!Q![*a!g!h(n#X#Y(n#a#b.T~*fT#OP!O!P(`!Q![*u!g!h(n#X#Y(n#a#b.T~*zT#OP!O!P(`!Q![+Z!g!h(n#X#Y(n#a#b.T~+`U#OP}!O+r!O!P(`!Q![-o!g!h(n#X#Y(n#a#b.T~+uP!Q![+x~+{P!Q![,O~,RP}!O,U~,XP!Q![,[~,_P!Q![,b~,eP!v!w,h~,kP!Q![,n~,qP!Q![,t~,wP![!],z~,}P!Q![-Q~-TP!Q![-W~-ZP![!]-^~-aP!Q![-d~-gP!Q![-j~-oO!j~~-tT#OP!O!P(`!Q![-o!g!h(n#X#Y(n#a#b.T~.WP#g#h.Z~.`O!h~~.eOZ~~.hR}!O.q!c!}.z#T#o.z~.tQ!c!}.z#T#o.z~/PSS~}!O.z!Q![.z!c!}.z#T#o.z~/dU!|~^~tu%y}!O%y!Q![%y!c!}/]#R#S%y#T#o/]~/{O!p~~0QO!o~~0VPV~#o#p0Y~0]Stu0i!c!}0i#R#S0i#T#o0i~0lXtu0i}!O0i!O!P0i!Q![0i![!]1X!c!}0i#R#S0i#T#o0i#q#r2]~1[UOY1XZ#q1X#q#r1n#r;'S1X;'S;=`2V<%lO1X~1qTO#q1X#q#r2Q#r;'S1X;'S;=`2V<%lO1X~2VO_~~2YP;=`<%l1X~2`P#q#r2Q~2hOU~",
|
||||||
tokenizers: [0],
|
tokenizers: [0, 1],
|
||||||
topRules: {"Document":[0,2]},
|
topRules: {"Document":[0,2]},
|
||||||
specialized: [{term: 4, get: (value: keyof typeof spec_AtKeyword) => spec_AtKeyword[value] || -1},{term: 73, get: (value: keyof typeof spec_identifier) => spec_identifier[value] || -1}],
|
specialized: [{term: 4, get: (value: keyof typeof spec_AtKeyword) => spec_AtKeyword[value] || -1},{term: 90, get: (value: keyof typeof spec_identifier) => spec_identifier[value] || -1}],
|
||||||
tokenPrec: 503
|
tokenPrec: 694
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,313 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { EditorState } from '@codemirror/state';
|
||||||
|
import { httpLanguage } from '../language';
|
||||||
|
import { parseHttpRequest } from './request-parser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建测试用的 EditorState
|
||||||
|
*/
|
||||||
|
function createTestState(content: string): EditorState {
|
||||||
|
return EditorState.create({
|
||||||
|
doc: content,
|
||||||
|
extensions: [httpLanguage]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('HTTP Request Parser - 新格式测试', () => {
|
||||||
|
|
||||||
|
describe('✅ @params - URL 参数', () => {
|
||||||
|
it('应该正确解析 params 请求', () => {
|
||||||
|
const content = `GET "https://api.example.com/users" {
|
||||||
|
@params {
|
||||||
|
page: 1,
|
||||||
|
size: 20,
|
||||||
|
keyword: "张三"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.method).toBe('GET');
|
||||||
|
expect(request?.url).toBe('https://api.example.com/users');
|
||||||
|
expect(request?.bodyType).toBe('params');
|
||||||
|
expect(request?.body).toEqual({
|
||||||
|
page: 1,
|
||||||
|
size: 20,
|
||||||
|
keyword: '张三'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('✅ @xml - XML 格式', () => {
|
||||||
|
it('应该正确解析 xml 请求', () => {
|
||||||
|
const content = `POST "https://api.example.com/soap" {
|
||||||
|
content-type: "application/xml"
|
||||||
|
|
||||||
|
@xml {
|
||||||
|
xml: "<user><name>张三</name><age>25</age></user>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.method).toBe('POST');
|
||||||
|
expect(request?.bodyType).toBe('xml');
|
||||||
|
expect(request?.body).toEqual({
|
||||||
|
xml: '<user><name>张三</name><age>25</age></user>'
|
||||||
|
});
|
||||||
|
expect(request?.headers['content-type']).toBe('application/xml');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确解析空 xml 块', () => {
|
||||||
|
const content = `POST "https://api.example.com/soap" {
|
||||||
|
@xml {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.bodyType).toBe('xml');
|
||||||
|
expect(request?.body).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('✅ @html - HTML 格式', () => {
|
||||||
|
it('应该正确解析 html 请求', () => {
|
||||||
|
const content = `POST "https://api.example.com/render" {
|
||||||
|
content-type: "text/html"
|
||||||
|
|
||||||
|
@html {
|
||||||
|
html: "<div><h1>标题</h1><p>内容</p></div>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.method).toBe('POST');
|
||||||
|
expect(request?.bodyType).toBe('html');
|
||||||
|
expect(request?.body).toEqual({
|
||||||
|
html: '<div><h1>标题</h1><p>内容</p></div>'
|
||||||
|
});
|
||||||
|
expect(request?.headers['content-type']).toBe('text/html');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确解析空 html 块', () => {
|
||||||
|
const content = `POST "https://api.example.com/render" {
|
||||||
|
@html {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.bodyType).toBe('html');
|
||||||
|
expect(request?.body).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('✅ @javascript - JavaScript 格式', () => {
|
||||||
|
it('应该正确解析 javascript 请求', () => {
|
||||||
|
const content = `POST "https://api.example.com/execute" {
|
||||||
|
content-type: "application/javascript"
|
||||||
|
|
||||||
|
@javascript {
|
||||||
|
javascript: "function hello() { return 'Hello World'; }"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.method).toBe('POST');
|
||||||
|
expect(request?.bodyType).toBe('javascript');
|
||||||
|
expect(request?.body).toEqual({
|
||||||
|
javascript: "function hello() { return 'Hello World'; }"
|
||||||
|
});
|
||||||
|
expect(request?.headers['content-type']).toBe('application/javascript');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确解析空 javascript 块', () => {
|
||||||
|
const content = `POST "https://api.example.com/execute" {
|
||||||
|
@javascript {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.bodyType).toBe('javascript');
|
||||||
|
expect(request?.body).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('✅ @binary - 二进制文件', () => {
|
||||||
|
it('应该正确解析 binary 请求', () => {
|
||||||
|
const content = `POST "https://api.example.com/upload" {
|
||||||
|
content-type: "application/octet-stream"
|
||||||
|
|
||||||
|
@binary {
|
||||||
|
binary: "@file E://Documents/avatar.png"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.method).toBe('POST');
|
||||||
|
expect(request?.bodyType).toBe('binary');
|
||||||
|
expect(request?.body).toEqual({
|
||||||
|
binary: '@file E://Documents/avatar.png'
|
||||||
|
});
|
||||||
|
expect(request?.headers['content-type']).toBe('application/octet-stream');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确解析空 binary 块', () => {
|
||||||
|
const content = `POST "https://api.example.com/upload" {
|
||||||
|
@binary {}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.bodyType).toBe('binary');
|
||||||
|
expect(request?.body).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('✅ 混合使用场景', () => {
|
||||||
|
it('应该正确解析带 params 和 headers 的请求', () => {
|
||||||
|
const content = `GET "https://api.example.com/search" {
|
||||||
|
authorization: "Bearer token123"
|
||||||
|
|
||||||
|
@params {
|
||||||
|
q: "关键词",
|
||||||
|
page: 1,
|
||||||
|
limit: 50
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.method).toBe('GET');
|
||||||
|
expect(request?.headers['authorization']).toBe('Bearer token123');
|
||||||
|
expect(request?.bodyType).toBe('params');
|
||||||
|
expect(request?.body).toEqual({
|
||||||
|
q: '关键词',
|
||||||
|
page: 1,
|
||||||
|
limit: 50
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确解析复杂 XML 内容', () => {
|
||||||
|
const content = `POST "https://api.example.com/soap" {
|
||||||
|
@xml {
|
||||||
|
xml: "<soap:Envelope xmlns:soap=\\"http://www.w3.org/2003/05/soap-envelope\\"><soap:Body><GetUser><UserId>123</UserId></GetUser></soap:Body></soap:Envelope>"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.bodyType).toBe('xml');
|
||||||
|
expect(request?.body.xml).toContain('soap:Envelope');
|
||||||
|
expect(request?.body.xml).toContain('GetUser');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('✅ 对比:传统格式仍然可用', () => {
|
||||||
|
it('JSON 格式仍然正常工作', () => {
|
||||||
|
const content = `POST "https://api.example.com/api" {
|
||||||
|
@json {
|
||||||
|
name: "张三",
|
||||||
|
age: 25,
|
||||||
|
email: "test@example.com"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.bodyType).toBe('json');
|
||||||
|
expect(request?.body).toEqual({
|
||||||
|
name: '张三',
|
||||||
|
age: 25,
|
||||||
|
email: 'test@example.com'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('FormData 格式仍然正常工作', () => {
|
||||||
|
const content = `POST "https://api.example.com/form" {
|
||||||
|
@formdata {
|
||||||
|
username: "admin",
|
||||||
|
password: "123456"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
const request = parseHttpRequest(state, 0);
|
||||||
|
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.bodyType).toBe('formdata');
|
||||||
|
expect(request?.body).toEqual({
|
||||||
|
username: 'admin',
|
||||||
|
password: '123456'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('✅ 多个请求解析', () => {
|
||||||
|
it('应该能在同一文档中解析不同格式的请求', () => {
|
||||||
|
const content = `POST "https://api.example.com/xml" {
|
||||||
|
@xml {
|
||||||
|
xml: "<user><name>张三</name></user>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POST "https://api.example.com/html" {
|
||||||
|
@html {
|
||||||
|
html: "<div>内容</div>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POST "https://api.example.com/js" {
|
||||||
|
@javascript {
|
||||||
|
javascript: "console.log('test');"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const state = createTestState(content);
|
||||||
|
|
||||||
|
// 解析第一个请求(XML)
|
||||||
|
const request1 = parseHttpRequest(state, 0);
|
||||||
|
expect(request1?.bodyType).toBe('xml');
|
||||||
|
expect(request1?.body.xml).toContain('张三');
|
||||||
|
|
||||||
|
// 解析第二个请求(HTML)- 找到第二个 POST 的位置
|
||||||
|
const secondPostIndex = content.indexOf('POST', 10);
|
||||||
|
const request2 = parseHttpRequest(state, secondPostIndex);
|
||||||
|
expect(request2?.bodyType).toBe('html');
|
||||||
|
expect(request2?.body.html).toContain('内容');
|
||||||
|
|
||||||
|
// 解析第三个请求(JavaScript)
|
||||||
|
const thirdPostIndex = content.indexOf('POST', secondPostIndex + 10);
|
||||||
|
const request3 = parseHttpRequest(state, thirdPostIndex);
|
||||||
|
expect(request3?.bodyType).toBe('javascript');
|
||||||
|
expect(request3?.body.javascript).toContain('console.log');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ export interface HttpRequest {
|
|||||||
headers: Record<string, string>;
|
headers: Record<string, string>;
|
||||||
|
|
||||||
/** 请求体类型 */
|
/** 请求体类型 */
|
||||||
bodyType?: 'json' | 'formdata' | 'urlencoded' | 'text';
|
bodyType?: 'json' | 'formdata' | 'urlencoded' | 'text' | 'params' | 'xml' | 'html' | 'javascript' | 'binary';
|
||||||
|
|
||||||
/** 请求体内容 */
|
/** 请求体内容 */
|
||||||
body?: any;
|
body?: any;
|
||||||
@@ -48,12 +48,30 @@ const NODE_TYPES = {
|
|||||||
FORMDATA_RULE: 'FormDataRule',
|
FORMDATA_RULE: 'FormDataRule',
|
||||||
URLENCODED_RULE: 'UrlEncodedRule',
|
URLENCODED_RULE: 'UrlEncodedRule',
|
||||||
TEXT_RULE: 'TextRule',
|
TEXT_RULE: 'TextRule',
|
||||||
|
PARAMS_RULE: 'ParamsRule',
|
||||||
|
XML_RULE: 'XmlRule',
|
||||||
|
HTML_RULE: 'HtmlRule',
|
||||||
|
JAVASCRIPT_RULE: 'JavaScriptRule',
|
||||||
|
BINARY_RULE: 'BinaryRule',
|
||||||
JSON_KEYWORD: 'JsonKeyword',
|
JSON_KEYWORD: 'JsonKeyword',
|
||||||
FORMDATA_KEYWORD: 'FormDataKeyword',
|
FORMDATA_KEYWORD: 'FormDataKeyword',
|
||||||
URLENCODED_KEYWORD: 'UrlEncodedKeyword',
|
URLENCODED_KEYWORD: 'UrlEncodedKeyword',
|
||||||
TEXT_KEYWORD: 'TextKeyword',
|
TEXT_KEYWORD: 'TextKeyword',
|
||||||
|
PARAMS_KEYWORD: 'ParamsKeyword',
|
||||||
|
XML_KEYWORD: 'XmlKeyword',
|
||||||
|
HTML_KEYWORD: 'HtmlKeyword',
|
||||||
|
JAVASCRIPT_KEYWORD: 'JavaScriptKeyword',
|
||||||
|
BINARY_KEYWORD: 'BinaryKeyword',
|
||||||
JSON_BLOCK: 'JsonBlock',
|
JSON_BLOCK: 'JsonBlock',
|
||||||
JSON_PROPERTY: 'JsonProperty',
|
JSON_PROPERTY: 'JsonProperty',
|
||||||
|
XML_BLOCK: 'XmlBlock',
|
||||||
|
HTML_BLOCK: 'HtmlBlock',
|
||||||
|
JAVASCRIPT_BLOCK: 'JavaScriptBlock',
|
||||||
|
BINARY_BLOCK: 'BinaryBlock',
|
||||||
|
XML_KEY: 'XmlKey',
|
||||||
|
HTML_KEY: 'HtmlKey',
|
||||||
|
JAVASCRIPT_KEY: 'JavaScriptKey',
|
||||||
|
BINARY_KEY: 'BinaryKey',
|
||||||
VARIABLE_REF: 'VariableRef',
|
VARIABLE_REF: 'VariableRef',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -220,17 +238,74 @@ export class HttpRequestParser {
|
|||||||
[NODE_TYPES.FORMDATA_RULE]: 'formdata',
|
[NODE_TYPES.FORMDATA_RULE]: 'formdata',
|
||||||
[NODE_TYPES.URLENCODED_RULE]: 'urlencoded',
|
[NODE_TYPES.URLENCODED_RULE]: 'urlencoded',
|
||||||
[NODE_TYPES.TEXT_RULE]: 'text',
|
[NODE_TYPES.TEXT_RULE]: 'text',
|
||||||
|
[NODE_TYPES.PARAMS_RULE]: 'params',
|
||||||
|
[NODE_TYPES.XML_RULE]: 'xml',
|
||||||
|
[NODE_TYPES.HTML_RULE]: 'html',
|
||||||
|
[NODE_TYPES.JAVASCRIPT_RULE]: 'javascript',
|
||||||
|
[NODE_TYPES.BINARY_RULE]: 'binary',
|
||||||
};
|
};
|
||||||
|
|
||||||
const type = typeMap[node.name];
|
const type = typeMap[node.name];
|
||||||
|
|
||||||
|
// 根据不同的规则类型解析不同的块
|
||||||
|
let content: any = null;
|
||||||
|
|
||||||
|
if (node.name === NODE_TYPES.XML_RULE) {
|
||||||
|
const blockNode = node.getChild(NODE_TYPES.XML_BLOCK);
|
||||||
|
content = blockNode ? this.parseFixedKeyBlock(blockNode, 'xml') : null;
|
||||||
|
} else if (node.name === NODE_TYPES.HTML_RULE) {
|
||||||
|
const blockNode = node.getChild(NODE_TYPES.HTML_BLOCK);
|
||||||
|
content = blockNode ? this.parseFixedKeyBlock(blockNode, 'html') : null;
|
||||||
|
} else if (node.name === NODE_TYPES.JAVASCRIPT_RULE) {
|
||||||
|
const blockNode = node.getChild(NODE_TYPES.JAVASCRIPT_BLOCK);
|
||||||
|
content = blockNode ? this.parseFixedKeyBlock(blockNode, 'javascript') : null;
|
||||||
|
} else if (node.name === NODE_TYPES.BINARY_RULE) {
|
||||||
|
const blockNode = node.getChild(NODE_TYPES.BINARY_BLOCK);
|
||||||
|
content = blockNode ? this.parseFixedKeyBlock(blockNode, 'binary') : null;
|
||||||
|
} else {
|
||||||
|
// json, formdata, urlencoded, text, params 使用 JsonBlock
|
||||||
const blockNode = node.getChild(NODE_TYPES.JSON_BLOCK);
|
const blockNode = node.getChild(NODE_TYPES.JSON_BLOCK);
|
||||||
|
content = blockNode ? this.parseJsonBlock(blockNode) : null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
content: blockNode ? this.parseJsonBlock(blockNode) : null
|
content
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析固定 key 的块(xml, html, javascript, binary)
|
||||||
|
* 格式:{ key: "value" } 或 {}(空块)
|
||||||
|
*/
|
||||||
|
private parseFixedKeyBlock(node: SyntaxNode, keyName: string): any {
|
||||||
|
// 查找固定的 key 节点
|
||||||
|
const keyNode = node.getChild(
|
||||||
|
keyName === 'xml' ? NODE_TYPES.XML_KEY :
|
||||||
|
keyName === 'html' ? NODE_TYPES.HTML_KEY :
|
||||||
|
keyName === 'javascript' ? NODE_TYPES.JAVASCRIPT_KEY :
|
||||||
|
NODE_TYPES.BINARY_KEY
|
||||||
|
);
|
||||||
|
|
||||||
|
// 如果没有 key,返回空对象(支持空块)
|
||||||
|
if (!keyNode) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找值节点(冒号后面的内容)
|
||||||
|
let value: any = null;
|
||||||
|
for (let child = node.firstChild; child; child = child.nextSibling) {
|
||||||
|
if (child.name === NODE_TYPES.STRING_LITERAL ||
|
||||||
|
child.name === NODE_TYPES.VARIABLE_REF) {
|
||||||
|
value = this.parseValue(child);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回格式:{ xml: "value" } 或 { html: "value" } 等
|
||||||
|
return value !== null ? { [keyName]: value } : {};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析 JsonBlock(用于 @json, @form, @urlencoded)
|
* 解析 JsonBlock(用于 @json, @form, @urlencoded)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type HttpRequest struct {
|
|||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Headers map[string]string `json:"headers"`
|
Headers map[string]string `json:"headers"`
|
||||||
BodyType string `json:"bodyType,omitempty"` // json, formdata, urlencoded, text
|
BodyType string `json:"bodyType,omitempty"` // json, formdata, urlencoded, text, params, xml, html, javascript, binary
|
||||||
Body interface{} `json:"body,omitempty"`
|
Body interface{} `json:"body,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +89,7 @@ func (hcs *HttpClientService) setRequestBody(req *resty.Request, request *HttpRe
|
|||||||
case "json":
|
case "json":
|
||||||
req.SetHeader("Content-Type", "application/json")
|
req.SetHeader("Content-Type", "application/json")
|
||||||
req.SetBody(request.Body)
|
req.SetBody(request.Body)
|
||||||
|
|
||||||
case "formdata":
|
case "formdata":
|
||||||
if formData, ok := request.Body.(map[string]interface{}); ok {
|
if formData, ok := request.Body.(map[string]interface{}); ok {
|
||||||
for key, value := range formData {
|
for key, value := range formData {
|
||||||
@@ -104,6 +105,7 @@ func (hcs *HttpClientService) setRequestBody(req *resty.Request, request *HttpRe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case "urlencoded":
|
case "urlencoded":
|
||||||
req.SetHeader("Content-Type", "application/x-www-form-urlencoded")
|
req.SetHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||||
if formData, ok := request.Body.(map[string]interface{}); ok {
|
if formData, ok := request.Body.(map[string]interface{}); ok {
|
||||||
@@ -113,9 +115,79 @@ func (hcs *HttpClientService) setRequestBody(req *resty.Request, request *HttpRe
|
|||||||
}
|
}
|
||||||
req.SetBody(values.Encode())
|
req.SetBody(values.Encode())
|
||||||
}
|
}
|
||||||
|
|
||||||
case "text":
|
case "text":
|
||||||
req.SetHeader("Content-Type", "text/plain")
|
req.SetHeader("Content-Type", "text/plain")
|
||||||
req.SetBody(fmt.Sprintf("%v", request.Body))
|
req.SetBody(fmt.Sprintf("%v", request.Body))
|
||||||
|
|
||||||
|
case "params":
|
||||||
|
// URL 参数:添加到查询字符串中
|
||||||
|
if params, ok := request.Body.(map[string]interface{}); ok {
|
||||||
|
for key, value := range params {
|
||||||
|
req.SetQueryParam(key, fmt.Sprintf("%v", value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "xml":
|
||||||
|
// XML 格式:从 Body 中提取 xml 字段
|
||||||
|
req.SetHeader("Content-Type", "application/xml")
|
||||||
|
if bodyMap, ok := request.Body.(map[string]interface{}); ok {
|
||||||
|
if xmlContent, exists := bodyMap["xml"]; exists {
|
||||||
|
req.SetBody(fmt.Sprintf("%v", xmlContent))
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("xml body type requires 'xml' field")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("xml body must be an object with 'xml' field")
|
||||||
|
}
|
||||||
|
|
||||||
|
case "html":
|
||||||
|
// HTML 格式:从 Body 中提取 html 字段
|
||||||
|
req.SetHeader("Content-Type", "text/html")
|
||||||
|
if bodyMap, ok := request.Body.(map[string]interface{}); ok {
|
||||||
|
if htmlContent, exists := bodyMap["html"]; exists {
|
||||||
|
req.SetBody(fmt.Sprintf("%v", htmlContent))
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("html body type requires 'html' field")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("html body must be an object with 'html' field")
|
||||||
|
}
|
||||||
|
|
||||||
|
case "javascript":
|
||||||
|
// JavaScript 格式:从 Body 中提取 javascript 字段
|
||||||
|
req.SetHeader("Content-Type", "application/javascript")
|
||||||
|
if bodyMap, ok := request.Body.(map[string]interface{}); ok {
|
||||||
|
if jsContent, exists := bodyMap["javascript"]; exists {
|
||||||
|
req.SetBody(fmt.Sprintf("%v", jsContent))
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("javascript body type requires 'javascript' field")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("javascript body must be an object with 'javascript' field")
|
||||||
|
}
|
||||||
|
|
||||||
|
case "binary":
|
||||||
|
// Binary 格式:从 Body 中提取 binary 字段(文件路径)
|
||||||
|
req.SetHeader("Content-Type", "application/octet-stream")
|
||||||
|
if bodyMap, ok := request.Body.(map[string]interface{}); ok {
|
||||||
|
if binaryContent, exists := bodyMap["binary"]; exists {
|
||||||
|
binaryStr := fmt.Sprintf("%v", binaryContent)
|
||||||
|
// 检查是否是文件类型,使用 @file 关键词
|
||||||
|
if strings.HasPrefix(binaryStr, "@file ") {
|
||||||
|
// 提取文件路径(去掉 @file 前缀)
|
||||||
|
filePath := strings.TrimSpace(strings.TrimPrefix(binaryStr, "@file "))
|
||||||
|
req.SetFile("file", filePath)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("binary body requires '@file path/to/file' format")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("binary body type requires 'binary' field")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("binary body must be an object with 'binary' field")
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported body type: %s", request.BodyType)
|
return fmt.Errorf("unsupported body type: %s", request.BodyType)
|
||||||
}
|
}
|
||||||
|
|||||||
212
internal/services/httpclient_service_test.go
Normal file
212
internal/services/httpclient_service_test.go
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHttpClientService_SetRequestBody(t *testing.T) {
|
||||||
|
logger := log.New()
|
||||||
|
service := NewHttpClientService(logger)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
request *HttpRequest
|
||||||
|
expectError bool
|
||||||
|
errorMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "JSON 请求",
|
||||||
|
request: &HttpRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "https://api.example.com/json",
|
||||||
|
BodyType: "json",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"name": "张三",
|
||||||
|
"age": 25,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "XML 请求 - 正确格式",
|
||||||
|
request: &HttpRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "https://api.example.com/xml",
|
||||||
|
BodyType: "xml",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"xml": "<user><name>张三</name></user>",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "XML 请求 - 缺少 xml 字段",
|
||||||
|
request: &HttpRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "https://api.example.com/xml",
|
||||||
|
BodyType: "xml",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"data": "<user><name>张三</name></user>",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
errorMsg: "xml body type requires 'xml' field",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HTML 请求 - 正确格式",
|
||||||
|
request: &HttpRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "https://api.example.com/html",
|
||||||
|
BodyType: "html",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"html": "<div><h1>标题</h1></div>",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "JavaScript 请求 - 正确格式",
|
||||||
|
request: &HttpRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "https://api.example.com/js",
|
||||||
|
BodyType: "javascript",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"javascript": "function hello() { return 'world'; }",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Binary 请求 - 正确格式",
|
||||||
|
request: &HttpRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "https://api.example.com/upload",
|
||||||
|
BodyType: "binary",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"binary": "@file C:/test.bin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Binary 请求 - 错误格式(缺少 @file)",
|
||||||
|
request: &HttpRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "https://api.example.com/upload",
|
||||||
|
BodyType: "binary",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"binary": "C:/test.bin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
errorMsg: "binary body requires '@file path/to/file' format",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Params 请求",
|
||||||
|
request: &HttpRequest{
|
||||||
|
Method: "GET",
|
||||||
|
URL: "https://api.example.com/users",
|
||||||
|
BodyType: "params",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"page": 1,
|
||||||
|
"size": 20,
|
||||||
|
"query": "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "FormData 请求",
|
||||||
|
request: &HttpRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "https://api.example.com/form",
|
||||||
|
BodyType: "formdata",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"username": "admin",
|
||||||
|
"password": "123456",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UrlEncoded 请求",
|
||||||
|
request: &HttpRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "https://api.example.com/login",
|
||||||
|
BodyType: "urlencoded",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"username": "admin",
|
||||||
|
"password": "123456",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Text 请求",
|
||||||
|
request: &HttpRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "https://api.example.com/text",
|
||||||
|
BodyType: "text",
|
||||||
|
Body: "纯文本内容",
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "不支持的 Body 类型",
|
||||||
|
request: &HttpRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "https://api.example.com/unknown",
|
||||||
|
BodyType: "unknown",
|
||||||
|
Body: "test",
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
errorMsg: "unsupported body type: unknown",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
req := service.client.R().SetContext(context.Background())
|
||||||
|
err := service.setRequestBody(req, tt.request)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
if tt.errorMsg != "" {
|
||||||
|
assert.Contains(t, err.Error(), tt.errorMsg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHttpClientService_FormatBytes(t *testing.T) {
|
||||||
|
logger := log.New()
|
||||||
|
service := NewHttpClientService(logger)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
bytes int64
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"负数", -1, "0 B"},
|
||||||
|
{"零字节", 0, "0 B"},
|
||||||
|
{"小于1KB", 500, "500 B"},
|
||||||
|
{"1KB", 1024, "1.0 KB"},
|
||||||
|
{"1MB", 1024 * 1024, "1.0 MB"},
|
||||||
|
{"1.5MB", 1024*1024 + 512*1024, "1.5 MB"},
|
||||||
|
{"1GB", 1024 * 1024 * 1024, "1.0 GB"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := service.formatBytes(tt.bytes)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user