feat(接口测试): 接口调试-导入curl

This commit is contained in:
baiqi 2024-01-24 09:36:25 +08:00 committed by Craftsman
parent 9de485fe9e
commit 8f4ddbbe87
13 changed files with 355 additions and 36 deletions

View File

@ -102,7 +102,7 @@ export const editorProps = {
// 是否显示字符集切换
showCharsetChange: {
type: Boolean as PropType<boolean>,
default: true,
default: false,
},
// 是否显示主题切换
showThemeChange: {

View File

@ -87,7 +87,7 @@
</div>
</template>
<template #cell="{ column, record, rowIndex }">
<div :class="{ 'flex flex-row items-center': !item.isTag && !item.align }">
<div :class="{ 'flex w-full flex-row items-center': !item.isTag && !item.align }">
<template v-if="item.dataIndex === SpecialColumnEnum.ENABLE">
<slot name="enable" v-bind="{ record }">
<div v-if="record.enable" class="flex flex-row flex-nowrap items-center gap-[2px]">

View File

@ -370,3 +370,69 @@ export function decodeStringToCharset(str: string, charset = 'UTF-8') {
const decoder = new TextDecoder(charset);
return decoder.decode(encoder.encode(str));
}
interface ParsedCurlOptions {
url?: string;
queryParameters?: { name: string; value: string }[];
headers?: { name: string; value: string }[];
}
/**
* curl
* @param curlScript curl
*/
export function parseCurlScript(curlScript: string): ParsedCurlOptions {
const options: ParsedCurlOptions = {};
// 提取 URL
const [_, url] = curlScript.match(/curl\s+'([^']+)'/) || [];
if (url) {
options.url = url;
}
// 提取 query 参数
const queryMatch = curlScript.match(/\?(.*?)'/);
if (queryMatch) {
const queryParams = queryMatch[1].split('&').map((param) => {
const [name, value] = param.split('=');
return { name, value };
});
options.queryParameters = queryParams;
}
// 提取 header
const headersMatch = curlScript.match(/-H\s+'([^']+)'/g);
if (headersMatch) {
const headers = headersMatch.map((header) => {
const [, value] = header.match(/-H\s+'([^']+)'/) || [];
const [name, rawValue] = value.split(':');
const trimmedName = name.trim();
const trimmedValue = rawValue ? rawValue.trim() : '';
return { name: trimmedName, value: trimmedValue };
});
// 过滤常用的 HTTP header
const commonHeaders = [
'accept',
'accept-language',
'cache-control',
'content-type',
'origin',
'pragma',
'referer',
'sec-ch-ua',
'sec-ch-ua-mobile',
'sec-ch-ua-platform',
'sec-fetch-dest',
'sec-fetch-mode',
'sec-fetch-site',
'user-agent',
'Connection',
'Host',
'Accept-Encoding',
'X-Requested-With',
];
options.headers = headers.filter((header) => !commonHeaders.includes(header.name.toLowerCase()));
}
return options;
}

View File

@ -485,7 +485,7 @@ org.apache.http.client.method . . . '' at line number 2
value: 'temp',
},
],
width: 110,
width: 130,
},
{
title: 'apiTestDebug.mode',

View File

@ -65,7 +65,7 @@
<a-select
v-model:model-value="record.type"
:options="columnConfig.typeOptions || []"
class="param-input"
class="param-input w-full"
@change="(val) => handleTypeChange(val, record)"
/>
</template>
@ -281,6 +281,8 @@
</template>
<script async setup lang="ts">
import { isEqual } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
@ -369,8 +371,7 @@
}
);
const emit = defineEmits<{
(e: 'update:params', value: any[]): void;
(e: 'change', data: any[], isInit?: boolean): void;
(e: 'change', data: any[], isInit?: boolean): void; //
(e: 'moreActionSelect', event: ActionsItem, record: Record<string, any>): void;
}>();
@ -397,24 +398,6 @@
isSimpleSetting: props.isSimpleSetting,
});
watch(
() => props.params,
(val) => {
if (val.length > 0) {
propsRes.value.data = val;
} else {
propsRes.value.data = props.params.concat({
id: new Date().getTime(), // id props.defaultParamItem id
...props.defaultParamItem,
});
emit('change', propsRes.value.data, true);
}
},
{
immediate: true,
}
);
watch(
() => props.heightUsed,
(val) => {
@ -440,12 +423,13 @@
key?: string,
isForce?: boolean
) {
const lastData = propsRes.value.data[propsRes.value.data.length - 1];
const lastData = { ...propsRes.value.data[propsRes.value.data.length - 1] };
delete lastData.id;
// key key
const isNotChange =
val === undefined || key === undefined
? Object.keys(props.defaultParamItem).every((e) => lastData[e] === props.defaultParamItem[e])
: JSON.stringify(lastData[key]) === JSON.stringify(props.defaultParamItem[key]);
? isEqual(lastData, props.defaultParamItem)
: isEqual(lastData[key], props.defaultParamItem[key]);
if (isForce || (val !== '' && !isNotChange)) {
propsRes.value.data.push({
id: new Date().getTime(),
@ -455,6 +439,32 @@
}
}
watch(
() => props.params,
(val) => {
if (val.length > 0) {
const lastData = { ...val[val.length - 1] };
delete lastData.id; // id
const isNotChange = isEqual(lastData, props.defaultParamItem);
propsRes.value.data = val;
if (!isNotChange) {
addTableLine();
}
} else {
propsRes.value.data = [
{
id: new Date().getTime(), // id props.defaultParamItem id
...props.defaultParamItem,
},
] as any[];
emit('change', propsRes.value.data, true);
}
},
{
immediate: true,
}
);
const showQuickInputParam = ref(false);
const activeQuickInputRecord = ref<any>({});
const quickInputParamValue = ref('');

View File

@ -72,7 +72,7 @@
{
title: 'apiTestDebug.maxConnection',
dataIndex: 'maxConnection',
width: 110,
width: 140,
},
{
title: 'apiTestDebug.timeout',

View File

@ -329,14 +329,20 @@ Date: Wed, 13 Dec 2023 08:53:25 GMT`,
activeDebug.value.unSaved = true;
}
function addDebugTab() {
function addDebugTab(defaultProps?: Partial<TabItem>) {
const id = `debug-${Date.now()}`;
debugTabs.value.push({
...cloneDeep(defaultDebugParams),
module: props.module,
id,
...defaultProps,
});
activeRequestTab.value = id;
nextTick(() => {
if (defaultProps) {
handleActiveDebugChange();
}
});
}
function closeDebugTab(tab: TabItem) {

View File

@ -82,6 +82,7 @@
title: 'apiTestDebug.paramLengthRange',
dataIndex: 'lengthRange',
slotName: 'lengthRange',
align: 'center',
width: 200,
},
{

View File

@ -82,6 +82,7 @@
title: 'apiTestDebug.paramLengthRange',
dataIndex: 'lengthRange',
slotName: 'lengthRange',
align: 'center',
width: 200,
},
{

View File

@ -120,7 +120,7 @@
modulesCount?: Record<string, number>; //
isExpandAll?: boolean; //
}>();
const emit = defineEmits(['init', 'change', 'newApi']);
const emit = defineEmits(['init', 'change', 'newApi', 'import']);
const appStore = useAppStore();
const { t } = useI18n();
@ -132,6 +132,7 @@
emit('newApi');
break;
case 'import':
emit('import');
break;
default:

View File

@ -3,7 +3,12 @@
<MsSplitBox :size="0.25" :max="0.5">
<template #first>
<div class="p-[24px]">
<moduleTree @init="(val) => (folderTree = val)" @new-api="newApi" @change="(val) => (activeModule = val)" />
<moduleTree
@init="(val) => (folderTree = val)"
@new-api="newApi"
@change="(val) => (activeModule = val)"
@import="importDrawerVisible = true"
/>
</div>
</template>
<template #second>
@ -13,23 +18,99 @@
</template>
</MsSplitBox>
</MsCard>
<MsDrawer
v-model:visible="importDrawerVisible"
:width="680"
:ok-disabled="curlCode.trim() === ''"
disabled-width-drag
@cancel="curlCode = ''"
@confirm="handleCurlImportConfirm"
>
<template #title>
<a-tooltip position="right" :content="t('apiTestDebug.importByCURLTip')">
{{ t('apiTestDebug.importByCURL') }}
<icon-exclamation-circle
class="ml-[4px] text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</template>
<div class="h-full">
<MsCodeEditor
v-if="importDrawerVisible"
v-model:model-value="curlCode"
theme="MS-text"
height="100%"
language="plaintext"
:show-theme-change="false"
:show-full-screen="false"
>
</MsCodeEditor>
</div>
</MsDrawer>
</template>
<script lang="ts" setup>
import MsCard from '@/components/pure/ms-card/index.vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import debug from './components/debug/index.vue';
import moduleTree from './components/moduleTree.vue';
import { useI18n } from '@/hooks/useI18n';
import { parseCurlScript } from '@/utils';
import { ModuleTreeNode } from '@/models/projectManagement/file';
import { RequestContentTypeEnum } from '@/enums/apiEnum';
const { t } = useI18n();
const debugRef = ref<InstanceType<typeof debug>>();
const activeModule = ref<string>('root');
const folderTree = ref<ModuleTreeNode[]>([]);
const importDrawerVisible = ref(false);
const curlCode = ref('');
function newApi() {
debugRef.value?.addDebugTab();
}
function handleCurlImportConfirm() {
const { url, headers, queryParameters } = parseCurlScript(curlCode.value);
debugRef.value?.addDebugTab({
url,
headerParams: headers?.map((e) => ({
required: false,
type: 'string',
min: undefined,
max: undefined,
contentType: RequestContentTypeEnum.TEXT,
tag: [],
desc: '',
encode: false,
enable: false,
mustContain: false,
...e,
})),
value: '',
queryParams: queryParameters?.map((e) => ({
required: false,
type: 'string',
min: undefined,
max: undefined,
contentType: RequestContentTypeEnum.TEXT,
tag: [],
desc: '',
encode: false,
enable: false,
mustContain: false,
...e,
})),
});
curlCode.value = '';
importDrawerVisible.value = false;
}
</script>
<style lang="less" scoped></style>

View File

@ -7,8 +7,8 @@ export default {
'apiTestDebug.noMatchModule': 'No matching module data yet',
'apiTestDebug.header': 'Header',
'apiTestDebug.body': 'Body',
'apiTestDebug.prefix': 'Prefix',
'apiTestDebug.post': 'Post',
'apiTestDebug.prefix': 'Precondition',
'apiTestDebug.post': 'Postcondition',
'apiTestDebug.assertion': 'Assertion',
'apiTestDebug.auth': 'Auth',
'apiTestDebug.setting': 'Setting',
@ -22,6 +22,15 @@ export default {
'apiTestDebug.paramValuePlaceholder': 'Starting with {at}, double-click to quickly enter',
'apiTestDebug.paramValuePreview': 'Parameter preview',
'apiTestDebug.desc': 'Description',
'apiTestDebug.paramRequired': 'Required',
'apiTestDebug.paramNotRequired': 'Optional',
'apiTestDebug.paramType': 'Param type',
'apiTestDebug.paramLengthRange': 'Length range',
'apiTestDebug.paramMin': 'Min',
'apiTestDebug.paramMax': 'Max',
'apiTestDebug.encode': 'Encoding',
'apiTestDebug.encodeTip1': 'On: Use encoding',
'apiTestDebug.encodeTip2': 'Off: No encoding is used',
'apiTestDebug.apply': 'Apply',
'apiTestDebug.batchAddParamsTip': 'Writing format: parameter name: parameter value; such as nama: natural',
'apiTestDebug.batchAddParamsTip2': 'Multiple records are separated by newlines.',
@ -29,4 +38,146 @@ export default {
'Parameter names in batch addition are repeated. By default, the last data is the latest data.',
'apiTestDebug.quickInputParamsTip': 'Support Mock/JMeter/Json/Text/String, etc.',
'apiTestDebug.descPlaceholder': 'Please enter content',
'apiTestDebug.noneBody': 'Request without Body',
'apiTestDebug.sendAsMainText': 'Send as main text',
'apiTestDebug.sendAsMainTextTip1':
'Enable: Directly read the file content and display it in the response body, such as: files in image format',
'apiTestDebug.sendAsMainTextTip2': 'Close: Return as download file',
'apiTestDebug.queryTip': 'In the address bar followed by ? The following parameters, such as updateapi?id=112',
'apiTestDebug.restTip': 'Parameters separated by slash/ in the address bar, such as updateapi/{id}',
'apiTestDebug.authType': 'Authentication',
'apiTestDebug.account': 'Account',
'apiTestDebug.accountRequired': 'Account cannot be empty',
'apiTestDebug.password': 'Password',
'apiTestDebug.passwordRequired': 'Password cannot be empty',
'apiTestDebug.commonPlaceholder': 'Please enter',
'apiTestDebug.connectTimeout': 'Connection timed out',
'apiTestDebug.responseTimeout': 'Response timeout',
'apiTestDebug.certificateAlias': 'Certificate alias',
'apiTestDebug.redirect': 'Redirect',
'apiTestDebug.follow': 'Follow',
'apiTestDebug.auto': 'Auto',
'apiTestDebug.precondition': 'Precondition',
'apiTestDebug.openGlobalPrecondition': 'Enable global precondition',
'apiTestDebug.openGlobalPreconditionTip':
'It is enabled by default. If it is disabled, the global precondition will not be executed when running this interface.',
'apiTestDebug.sql': 'SQL',
'apiTestDebug.sqlScript': 'SQL script',
'apiTestDebug.waitTime': 'Wait time',
'apiTestDebug.script': 'Script',
'apiTestDebug.preconditionScriptName': 'Pre-script name',
'apiTestDebug.preconditionScriptNamePlaceholder': 'Please enter the pre-script name',
'apiTestDebug.manual': 'Manual entry',
'apiTestDebug.quote': 'Quoting public scripts',
'apiTestDebug.commonScriptList': 'Public script list',
'apiTestDebug.scriptEx': 'Script case',
'apiTestDebug.copyNotSupport': 'Your browser does not support automatic copying, please copy manually',
'apiTestDebug.scriptExCopySuccess': 'Script case copied',
'apiTestDebug.parameters': 'Pass parameters',
'apiTestDebug.scriptContent': 'Script content',
'apiTestDebug.introduceSource': 'Introduce data sources',
'apiTestDebug.quoteSource': 'Reference data source',
'apiTestDebug.sourceList': 'Data source list',
'apiTestDebug.quoteSourcePlaceholder': 'Please select a data source',
'apiTestDebug.storageType': 'Storage method',
'apiTestDebug.storageTypeTip1':
'Store by column: Specify the names of columns extracted from the database result set; multiple columns can be separated by ","',
'apiTestDebug.storageTypeTip2':
'Store by result: Save the entire result set as a variable instead of saving each column value as a separate variable',
'apiTestDebug.storageByCol': 'Store by columns',
'apiTestDebug.storageByColPlaceholder': 'For example, {a} is changed to {b}',
'apiTestDebug.storageByResult': 'Store by result',
'apiTestDebug.storageByResultPlaceholder': 'Such as {a}',
'apiTestDebug.extractParameter': 'Extract',
'apiTestDebug.searchTip': 'Please enter a group name',
'apiTestDebug.allRequest': 'All requests',
'apiTestDebug.deleteFolderTipTitle': 'Remove the `{name}` module?',
'apiTestDebug.deleteFolderTipContent':
'This operation will delete the module and all resources under it, please operate with caution!',
'apiTestDebug.deleteConfirm': 'Confirm delete',
'apiTestDebug.deleteSuccess': 'Successfully deleted',
'apiTestDebug.moduleMoveSuccess': 'Module moved successfully',
'apiTestDebug.sqlSourceName': 'Data source name',
'apiTestDebug.driver': 'Drive',
'apiTestDebug.username': 'Username',
'apiTestDebug.maxConnection': 'Max connections',
'apiTestDebug.timeout': 'Timeout (ms)',
'apiTestDebug.postCondition': 'Postcondition',
'apiTestDebug.openGlobalPostCondition': 'Enable global postcondition',
'apiTestDebug.openGlobalPostConditionTip':
'It is enabled by default. If it is disabled, the global post-processing will not be executed when running this interface.',
'apiTestDebug.globalParameter': 'Global parameter',
'apiTestDebug.envParameter': 'Env parameters',
'apiTestDebug.tempParameter': 'Temporary parameters',
'apiTestDebug.mode': 'Type',
'apiTestDebug.range': 'Scope',
'apiTestDebug.expression': 'Expression',
'apiTestDebug.expressionTip1': 'Reason for unavailability:',
'apiTestDebug.expressionTip2': '1. Interface not implemented',
'apiTestDebug.expressionTip3': '2. There is no data in the response content',
'apiTestDebug.regular': 'Regular',
'apiTestDebug.fastExtraction': 'Quick extraction',
'apiTestDebug.regularExpression': 'Regular expression',
'apiTestDebug.regularExpressionRequired': 'Regular expression cannot be empty',
'apiTestDebug.regularExpressionPlaceholder': 'Such as {ex}',
'apiTestDebug.test': 'Test',
'apiTestDebug.JSONPathRequired': 'JSONPath cannot be empty',
'apiTestDebug.JSONPathPlaceholder': 'Such as $.users',
'apiTestDebug.XPathRequired': 'XPath cannot be empty',
'apiTestDebug.XPathPlaceholder': 'Such as /books/book[1]/title',
'apiTestDebug.matchResult': 'Match results',
'apiTestDebug.noMatchResult': 'No matching results',
'apiTestDebug.matchExpressionTip':
'{prefix} Gets the complete expression for matching, including all tag contents in the expression',
'apiTestDebug.matchGroupTip':
'{prefix} Gets the matching group in the expression for matching, including only the regular content in the expression',
'apiTestDebug.matchExpression': 'Matching expression',
'apiTestDebug.matchGroup': 'Matching group',
'apiTestDebug.moreSetting': 'More settings',
'apiTestDebug.expressionMatchRule': 'Expression matching rules',
'apiTestDebug.resultMatchRule': 'Result matching rules',
'apiTestDebug.randomMatch': 'Random match',
'apiTestDebug.randomMatchTip': 'Get any matching result',
'apiTestDebug.specifyMatch': 'Specify match',
'apiTestDebug.specifyMatchResult': 'Specify matching results',
'apiTestDebug.index': 'No.',
'apiTestDebug.unit': 'item',
'apiTestDebug.specifyMatchTip':
'The Nth matching result needs to be specified. If it exceeds the specified number, it will return empty.',
'apiTestDebug.allMatch': 'Match all',
'apiTestDebug.allMatchTip': 'The regular return is an array of matching results.',
'apiTestDebug.contentType': 'Response content format',
'apiTestDebug.responseTime': 'Response time',
'apiTestDebug.responseStage': 'Stage',
'apiTestDebug.time': 'Duration',
'apiTestDebug.ready': 'Preparation stage',
'apiTestDebug.socketInit': 'Socket init',
'apiTestDebug.dnsQuery': 'DNS query',
'apiTestDebug.tcpHandshake': 'TCP handshake',
'apiTestDebug.sslHandshake': 'SSL handshake',
'apiTestDebug.waitingTTFB': 'Waiting (TTFB)',
'apiTestDebug.downloadContent': 'Content download',
'apiTestDebug.deal': 'Deal with',
'apiTestDebug.total': 'Total',
'apiTestDebug.responseBody': 'Response body',
'apiTestDebug.responseHeader': 'Response header',
'apiTestDebug.realRequest': 'Real request',
'apiTestDebug.console': 'Console',
'apiTestDebug.extract': 'Extract',
'apiTestDebug.statusCode': 'Status code',
'apiTestDebug.responseSize': 'Response size',
'apiTestDebug.runningEnv': 'Operating environment',
'apiTestDebug.resourcePool': 'Resource pool',
'apiTestDebug.content': 'Content',
'apiTestDebug.status': 'Status',
'apiTestDebug.requestName': 'Request name',
'apiTestDebug.requestNameRequired': 'Request name cannot be empty',
'apiTestDebug.requestNamePlaceholder': 'Please enter a request name',
'apiTestDebug.requestUrl': 'Request URL',
'apiTestDebug.requestUrlRequired': 'Request URL cannot be empty',
'apiTestDebug.requestModule': 'Belonging module',
'apiTestDebug.closeOther': 'Close other',
'apiTestDebug.importByCURL': 'Import cURL',
'apiTestDebug.importByCURLTip':
'Supports quick import of packet capture data from tools such as Chrome, Charles or Fiddler',
};

View File

@ -63,7 +63,7 @@ export default {
'apiTestDebug.waitTime': '等待时间',
'apiTestDebug.script': '脚本操作',
'apiTestDebug.preconditionScriptName': '前置脚本名称',
'apiTestDebug.preconditionScriptNamePlaceholder': '前置脚本名称',
'apiTestDebug.preconditionScriptNamePlaceholder': '请输入前置脚本名称',
'apiTestDebug.manual': '手动录入',
'apiTestDebug.quote': '引用公共脚本',
'apiTestDebug.commonScriptList': '公共脚本列表',
@ -139,8 +139,8 @@ export default {
'apiTestDebug.contentType': '响应内容格式',
'apiTestDebug.responseTime': '响应时间',
'apiTestDebug.responseStage': '阶段',
'apiTestDebug.time': '',
'apiTestDebug.ready': '准备',
'apiTestDebug.time': '时',
'apiTestDebug.ready': '准备阶段',
'apiTestDebug.socketInit': 'Socket 初始化',
'apiTestDebug.dnsQuery': 'DNS 查询',
'apiTestDebug.tcpHandshake': 'TCP 握手',
@ -167,4 +167,6 @@ export default {
'apiTestDebug.requestUrlRequired': '请求 URL不能为空',
'apiTestDebug.requestModule': '请求所属模块',
'apiTestDebug.closeOther': '关闭其他请求',
'apiTestDebug.importByCURL': '导入 cURL',
'apiTestDebug.importByCURLTip': '支持快速导入 Chrome、Charles 或 Fiddler 等工具中的抓包数据',
};