mirror of
https://gitee.com/fit2cloud-feizhiyun/MeterSphere.git
synced 2024-12-05 05:29:29 +08:00
refactor(接口测试): 优化jsonpath推荐
This commit is contained in:
parent
42ef1bcc89
commit
8efda5ef5e
@ -17,7 +17,7 @@ public class JsonPathUtils {
|
||||
|
||||
public static List<HashMap> getListJson(String jsonString) {
|
||||
|
||||
JSONObject jsonObject =JSONObject.parseObject(jsonString);
|
||||
JSONObject jsonObject = JSONObject.parseObject(jsonString);
|
||||
List<HashMap> allJsons =new ArrayList<>();
|
||||
|
||||
// 获取到所有jsonpath后,获取所有的key
|
||||
@ -40,127 +40,87 @@ public class JsonPathUtils {
|
||||
jsonPaths.remove(parentNodeJsonPath);
|
||||
}
|
||||
|
||||
List<String> jsonPathList = new ArrayList<>();
|
||||
Iterator<String> jsonPath = jsonPaths.iterator();
|
||||
//将/替换为点.
|
||||
while (jsonPath.hasNext()) {
|
||||
Map<String,String> item = new HashMap<>();
|
||||
|
||||
|
||||
String o_json_path = "$" + jsonPath.next().replaceAll("/", ".");
|
||||
String value = JSONPath.eval(jsonObject,o_json_path).toString();
|
||||
String value = JSONPath.eval(jsonObject, o_json_path).toString();
|
||||
|
||||
if(o_json_path.toLowerCase().contains("id")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if(value.equals("") || value.equals("[]") || o_json_path.equals("")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String json_path = formatJson(o_json_path);
|
||||
|
||||
|
||||
|
||||
//System.out.println(json_path);
|
||||
|
||||
|
||||
|
||||
item.put("json_path", json_path);
|
||||
item.put("json_value", addEscapeForString(value));
|
||||
allJsons.add((HashMap)item);
|
||||
|
||||
jsonPathList.add(json_path);
|
||||
}
|
||||
//排序
|
||||
Collections.sort(jsonPathList);
|
||||
|
||||
Collections.sort(allJsons, (a, b) ->
|
||||
( (String)a.get("json_path") )
|
||||
.compareTo( (String)b.get("json_path") )
|
||||
);
|
||||
|
||||
return allJsons;
|
||||
}
|
||||
|
||||
private static String formatJson(String json_path){
|
||||
|
||||
String ret="";
|
||||
// 正则表达式
|
||||
String reg = ".(\\d{1,3}).{0,1}";
|
||||
|
||||
Boolean change_flag = false;
|
||||
Matcher m1 = Pattern.compile(reg).matcher(json_path);
|
||||
|
||||
|
||||
String newStr="";
|
||||
int rest = 0;
|
||||
String tail = "";
|
||||
while (m1.find()) {
|
||||
|
||||
while (m1.find()) {
|
||||
int start = m1.start();
|
||||
int end = m1.end() - 1;
|
||||
if(json_path.charAt(start) != '.' || json_path.charAt(end) != '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
newStr += json_path.substring(rest,m1.start()) +"[*]." ;
|
||||
newStr += json_path.substring(rest,m1.start()) +"[" + json_path.substring(start + 1, end) + "]." ;
|
||||
|
||||
rest = m1.end();
|
||||
tail = json_path.substring(m1.end());
|
||||
change_flag = true;
|
||||
}
|
||||
|
||||
|
||||
if(change_flag) {
|
||||
ret = newStr + tail;
|
||||
} else {
|
||||
ret = json_path;
|
||||
}
|
||||
|
||||
|
||||
|
||||
return ret;
|
||||
|
||||
|
||||
}
|
||||
|
||||
private static String addEscapeForString(String input) {
|
||||
|
||||
String ret="";
|
||||
|
||||
|
||||
String reg = "[?*/]";
|
||||
|
||||
Boolean change_flag = false;
|
||||
Matcher m1 = Pattern.compile(reg).matcher(input);
|
||||
|
||||
|
||||
String newStr="";
|
||||
int rest = 0;
|
||||
String tail = "";
|
||||
|
||||
while (m1.find()) {
|
||||
|
||||
int start = m1.start();
|
||||
int end = m1.end() - 1;
|
||||
|
||||
|
||||
|
||||
newStr += input.substring(rest,m1.start()) + "\\" + m1.group(0) ;
|
||||
|
||||
rest = m1.end();
|
||||
tail = input.substring(m1.end());
|
||||
change_flag = true;
|
||||
|
||||
}
|
||||
if(change_flag) {
|
||||
ret = newStr + tail;
|
||||
} else {
|
||||
ret = input;
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,141 +1,142 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="assertion-add">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="4">
|
||||
<el-select :disabled="isReadOnly" class="assertion-item" v-model="type" :placeholder="$t('api_test.request.assertions.select_type')"
|
||||
size="small">
|
||||
<el-option :label="$t('api_test.request.assertions.text')" :value="options.TEXT"/>
|
||||
<el-option :label="$t('api_test.request.assertions.regex')" :value="options.REGEX"/>
|
||||
<el-option :label="'JSONPath'" :value="options.JSON_PATH"/>
|
||||
<el-option :label="$t('api_test.request.assertions.response_time')" :value="options.DURATION"/>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="20">
|
||||
<ms-api-assertion-text :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.TEXT" :callback="after"/>
|
||||
<ms-api-assertion-regex :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.REGEX" :callback="after"/>
|
||||
<ms-api-assertion-json-path :is-read-only="isReadOnly" :list="assertions.jsonPath" v-if="type === options.JSON_PATH" :callback="after"/>
|
||||
<ms-api-assertion-duration :is-read-only="isReadOnly" v-model="time" :duration="assertions.duration"
|
||||
v-if="type === options.DURATION" :callback="after"/>
|
||||
<el-button v-if="!type" :disabled="true" type="primary" size="small">Add</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div >
|
||||
|
||||
<el-row :gutter="10" style="text-align: right;">
|
||||
|
||||
<el-button
|
||||
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="suggestJson"
|
||||
>推荐JSONPath断言</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="clearJson"
|
||||
>清空JSONPath断言</el-button>
|
||||
|
||||
</el-row>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<ms-api-assertions-edit :is-read-only="isReadOnly" :assertions="assertions"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MsApiAssertionText from "./ApiAssertionText";
|
||||
import MsApiAssertionRegex from "./ApiAssertionRegex";
|
||||
import MsApiAssertionDuration from "./ApiAssertionDuration";
|
||||
import {ASSERTION_TYPE, Assertions, JSONPath} from "../../model/ScenarioModel";
|
||||
import MsApiAssertionsEdit from "./ApiAssertionsEdit";
|
||||
import MsApiAssertionJsonPath from "./ApiAssertionJsonPath";
|
||||
|
||||
export default {
|
||||
name: "MsApiAssertions",
|
||||
|
||||
components: {
|
||||
MsApiAssertionJsonPath,
|
||||
MsApiAssertionsEdit, MsApiAssertionDuration, MsApiAssertionRegex, MsApiAssertionText},
|
||||
|
||||
props: {
|
||||
assertions: Assertions,
|
||||
jsonPathList: Array,
|
||||
isReadOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
options: ASSERTION_TYPE,
|
||||
time: "",
|
||||
type: "",
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
after() {
|
||||
this.type = "";
|
||||
},
|
||||
suggestJson() {
|
||||
console.log("This is suggestJson")
|
||||
// console.log(this.jsonPathList);
|
||||
this.jsonPathList.forEach((item) => {
|
||||
let jsonItem = new JSONPath();
|
||||
jsonItem.expression=item.json_path;
|
||||
jsonItem.expect=item.json_value;
|
||||
jsonItem.setJSONPathDescription();
|
||||
this.assertions.jsonPath.push(jsonItem);
|
||||
});
|
||||
|
||||
},
|
||||
clearJson() {
|
||||
console.log("This is suggestJson")
|
||||
// console.log(this.jsonPathList);
|
||||
this.assertions.jsonPath = [];
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.assertion-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.assertion-add {
|
||||
padding: 10px;
|
||||
border: #DCDFE6 solid 1px;
|
||||
margin: 5px 0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.bg-purple-dark {
|
||||
background: #99a9bf;
|
||||
}
|
||||
.bg-purple {
|
||||
background: #d3dce6;
|
||||
}
|
||||
.bg-purple-light {
|
||||
background: #e5e9f2;
|
||||
}
|
||||
.grid-content {
|
||||
border-radius: 4px;
|
||||
min-height: 36px;
|
||||
}
|
||||
.row-bg {
|
||||
padding: 10px 0;
|
||||
background-color: #f9fafc;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div>
|
||||
<div class="assertion-add">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="4">
|
||||
<el-select :disabled="isReadOnly" class="assertion-item" v-model="type" :placeholder="$t('api_test.request.assertions.select_type')"
|
||||
size="small">
|
||||
<el-option :label="$t('api_test.request.assertions.text')" :value="options.TEXT"/>
|
||||
<el-option :label="$t('api_test.request.assertions.regex')" :value="options.REGEX"/>
|
||||
<el-option :label="'JSONPath'" :value="options.JSON_PATH"/>
|
||||
<el-option :label="$t('api_test.request.assertions.response_time')" :value="options.DURATION"/>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="20">
|
||||
<ms-api-assertion-text :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.TEXT" :callback="after"/>
|
||||
<ms-api-assertion-regex :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.REGEX" :callback="after"/>
|
||||
<ms-api-assertion-json-path :is-read-only="isReadOnly" :list="assertions.jsonPath" v-if="type === options.JSON_PATH" :callback="after"/>
|
||||
<ms-api-assertion-duration :is-read-only="isReadOnly" v-model="time" :duration="assertions.duration"
|
||||
v-if="type === options.DURATION" :callback="after"/>
|
||||
<el-button v-if="!type" :disabled="true" type="primary" size="small">Add</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<el-row :gutter="10" class="json-path-suggest-button">
|
||||
<el-button size="small" type="primary" @click="suggestJsonOpen">
|
||||
{{$t('api_test.request.assertions.json_path_suggest')}}
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" @click="clearJson">
|
||||
{{$t('api_test.request.assertions.json_path_clear')}}
|
||||
</el-button>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<ms-api-jsonpath-suggest-list @addJsonpathSuggest="addJsonpathSuggest" :request="request" ref="jsonpathSuggestList"/>
|
||||
|
||||
<ms-api-assertions-edit :is-read-only="isReadOnly" :assertions="assertions"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MsApiAssertionText from "./ApiAssertionText";
|
||||
import MsApiAssertionRegex from "./ApiAssertionRegex";
|
||||
import MsApiAssertionDuration from "./ApiAssertionDuration";
|
||||
import {ASSERTION_TYPE, Assertions, HttpRequest, JSONPath} from "../../model/ScenarioModel";
|
||||
import MsApiAssertionsEdit from "./ApiAssertionsEdit";
|
||||
import MsApiAssertionJsonPath from "./ApiAssertionJsonPath";
|
||||
import MsApiJsonpathSuggestList from "./ApiJsonpathSuggestList";
|
||||
|
||||
export default {
|
||||
name: "MsApiAssertions",
|
||||
|
||||
components: {
|
||||
MsApiJsonpathSuggestList,
|
||||
MsApiAssertionJsonPath,
|
||||
MsApiAssertionsEdit, MsApiAssertionDuration, MsApiAssertionRegex, MsApiAssertionText},
|
||||
|
||||
props: {
|
||||
assertions: Assertions,
|
||||
request: HttpRequest,
|
||||
isReadOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
options: ASSERTION_TYPE,
|
||||
time: "",
|
||||
type: "",
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
after() {
|
||||
this.type = "";
|
||||
},
|
||||
suggestJsonOpen() {
|
||||
if (!this.request.debugRequestResult) {
|
||||
this.$message(this.$t('api_test.request.assertions.debug_first'));
|
||||
return;
|
||||
}
|
||||
this.$refs.jsonpathSuggestList.open();
|
||||
},
|
||||
addJsonpathSuggest(jsonPathList) {
|
||||
jsonPathList.forEach(jsonPath => {
|
||||
let jsonItem = new JSONPath();
|
||||
jsonItem.expression = jsonPath.json_path;
|
||||
jsonItem.expect = jsonPath.json_value;
|
||||
jsonItem.setJSONPathDescription();
|
||||
this.assertions.jsonPath.push(jsonItem);
|
||||
});
|
||||
},
|
||||
clearJson() {
|
||||
this.assertions.jsonPath = [];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.assertion-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.assertion-add {
|
||||
padding: 10px;
|
||||
border: #DCDFE6 solid 1px;
|
||||
margin: 5px 0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.bg-purple-dark {
|
||||
background: #99a9bf;
|
||||
}
|
||||
|
||||
.bg-purple {
|
||||
background: #d3dce6;
|
||||
}
|
||||
|
||||
.bg-purple-light {
|
||||
background: #e5e9f2;
|
||||
}
|
||||
|
||||
.grid-content {
|
||||
border-radius: 4px;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.row-bg {
|
||||
padding: 10px 0;
|
||||
background-color: #f9fafc;
|
||||
}
|
||||
|
||||
.json-path-suggest-button {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<el-dialog :title="$t('api_test.request.assertions.json_path_add')"
|
||||
:visible.sync="dialogFormVisible"
|
||||
@close="close"
|
||||
width="60%" v-loading="result.loading"
|
||||
:close-on-click-modal="false"
|
||||
top="50px">
|
||||
|
||||
<el-container class="main-content">
|
||||
|
||||
<el-container>
|
||||
<el-main class="case-content">
|
||||
<el-table
|
||||
:data="jsonPathList"
|
||||
row-key="id"
|
||||
@select-all="handleSelectAll"
|
||||
@select="handleSelectionChange"
|
||||
height="50vh"
|
||||
ref="table">
|
||||
|
||||
<el-table-column type="selection"/>
|
||||
|
||||
<el-table-column
|
||||
prop="name"
|
||||
:label="$t('api_test.request.extract.json_path_expression')"
|
||||
style="width: 100%">
|
||||
<template v-slot:default="scope">
|
||||
{{scope.row.json_path}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="name"
|
||||
:label="$t('api_test.request.assertions.value')"
|
||||
style="width: 100%">
|
||||
<template v-slot:default="scope">
|
||||
{{scope.row.json_value}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
|
||||
<template v-slot:footer>
|
||||
<ms-dialog-footer @cancel="dialogFormVisible = false" @confirm="commit"/>
|
||||
</template>
|
||||
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MsDialogFooter from "../../../../common/components/MsDialogFooter";
|
||||
import {HttpRequest} from "../../model/ScenarioModel";
|
||||
export default {
|
||||
name: "MsApiJsonpathSuggestList",
|
||||
components: {MsDialogFooter},
|
||||
data() {
|
||||
return {
|
||||
result: {},
|
||||
dialogFormVisible: false,
|
||||
isCheckAll: false,
|
||||
selectItems: new Set(),
|
||||
jsonPathList: [],
|
||||
};
|
||||
},
|
||||
props: {
|
||||
request: HttpRequest,
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.selectItems.clear();
|
||||
},
|
||||
open() {
|
||||
this.getJsonPaths();
|
||||
},
|
||||
getJsonPaths() {
|
||||
if (this.request.debugRequestResult) {
|
||||
let param = {
|
||||
jsonPath: this.request.debugRequestResult.responseResult.body
|
||||
};
|
||||
this.result = this.$post("/api/getJsonPaths", param).then(response => {
|
||||
this.jsonPathList = response.data.data;
|
||||
this.dialogFormVisible = true;
|
||||
}).catch(() => {
|
||||
this.$warning(this.$t('api_test.request.assertions.json_path_err'));
|
||||
this.dialogFormVisible = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
handleSelectAll(selection) {
|
||||
if (selection.length > 0) {
|
||||
this.selectItems = new Set(this.jsonPathList);
|
||||
} else {
|
||||
this.selectItems = new Set();
|
||||
}
|
||||
},
|
||||
handleSelectionChange(selection, row) {
|
||||
if (this.selectItems.has(row)) {
|
||||
this.selectItems.delete(row);
|
||||
} else {
|
||||
this.selectItems.add(row);
|
||||
}
|
||||
},
|
||||
commit() {
|
||||
this.$emit("addJsonpathSuggest", this.selectItems);
|
||||
this.dialogFormVisible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -1,237 +1,237 @@
|
||||
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
|
||||
<el-form :model="request" :rules="rules" ref="request" label-width="100px" :disabled="isReadOnly">
|
||||
|
||||
<el-form-item :label="$t('api_test.request.name')" prop="name">
|
||||
<el-input :disabled="isReadOnly" v-model="request.name" maxlength="300" show-word-limit/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="!request.useEnvironment" :label="$t('api_test.request.url')" prop="url"
|
||||
class="adjust-margin-bottom">
|
||||
<el-input :disabled="isReadOnly" v-model="request.url" maxlength="500"
|
||||
:placeholder="$t('api_test.request.url_description')" @change="urlChange" clearable>
|
||||
<template v-slot:prepend>
|
||||
<ApiRequestMethodSelect :is-read-only="isReadOnly" :request="request" @change="methodChange"/>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="request.useEnvironment" :label="$t('api_test.request.path')" prop="path">
|
||||
<el-input :disabled="isReadOnly" v-model="request.path" maxlength="500"
|
||||
:placeholder="$t('api_test.request.path_description')" @change="pathChange" clearable>
|
||||
<template v-slot:prepend>
|
||||
<ApiRequestMethodSelect :is-read-only="isReadOnly" :request="request" @change="methodChange"/>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="request.useEnvironment" :label="$t('api_test.request.address')" class="adjust-margin-bottom">
|
||||
<el-tag class="environment-display">
|
||||
<span class="environment-name">{{ scenario.environment ? scenario.environment.name + ': ' : '' }}</span>
|
||||
<span class="environment-url">{{ displayUrl }}</span>
|
||||
<span v-if="!displayUrl"
|
||||
class="environment-url-tip">{{ $t('api_test.request.please_configure_socket_in_environment') }}</span>
|
||||
</el-tag>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-switch
|
||||
v-model="request.useEnvironment"
|
||||
:active-text="$t('api_test.request.refer_to_environment')" @change="useEnvironmentChange">
|
||||
</el-switch>
|
||||
<el-checkbox class="follow-redirects-item" v-model="request.followRedirects">{{$t('api_test.request.follow_redirects')}}</el-checkbox>
|
||||
<el-checkbox class="do-multipart-post" v-model="request.doMultipartPost">{{$t('api_test.request.do_multipart_post')}}</el-checkbox>
|
||||
</el-form-item>
|
||||
|
||||
<el-button :disabled="!request.enable || !scenario.enable || isReadOnly" class="debug-button" size="small"
|
||||
type="primary" @click="runDebug">{{ $t('api_test.request.debug') }}
|
||||
</el-button>
|
||||
|
||||
<el-tabs v-model="activeName">
|
||||
<el-tab-pane :label="$t('api_test.request.parameters')" name="parameters">
|
||||
<ms-api-variable :is-read-only="isReadOnly"
|
||||
:parameters="request.parameters"
|
||||
:environment="scenario.environment"
|
||||
:scenario="scenario"
|
||||
:extract="request.extract"
|
||||
:description="$t('api_test.request.parameters_desc')"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('api_test.request.headers')" name="headers">
|
||||
<ms-api-key-value :is-read-only="isReadOnly" :isShowEnable="true" :suggestions="headerSuggestions" :items="request.headers"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('api_test.request.body')" name="body">
|
||||
<ms-api-body :is-read-only="isReadOnly"
|
||||
:body="request.body"
|
||||
:scenario="scenario"
|
||||
:extract="request.extract"
|
||||
:environment="scenario.environment"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('api_test.request.assertions.label')" name="assertions">
|
||||
<ms-api-assertions :jsonPathList="jsonPathList" :is-read-only="isReadOnly" :assertions="request.assertions"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('api_test.request.extract.label')" name="extract">
|
||||
<ms-api-extract :is-read-only="isReadOnly" :extract="request.extract"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('api_test.request.processor.pre_exec_script')" name="jsr223PreProcessor">
|
||||
<ms-jsr233-processor :is-read-only="isReadOnly" :jsr223-processor="request.jsr223PreProcessor"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('api_test.request.processor.post_exec_script')" name="jsr223PostProcessor">
|
||||
<ms-jsr233-processor :is-read-only="isReadOnly" :jsr223-processor="request.jsr223PostProcessor"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('api_test.request.timeout_config')" name="advancedConfig">
|
||||
<ms-api-advanced-config :is-read-only="isReadOnly" :request="request"/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MsApiKeyValue from "../ApiKeyValue";
|
||||
import MsApiBody from "../body/ApiBody";
|
||||
import MsApiAssertions from "../assertion/ApiAssertions";
|
||||
import {HttpRequest, KeyValue, Scenario} from "../../model/ScenarioModel";
|
||||
import MsApiExtract from "../extract/ApiExtract";
|
||||
import ApiRequestMethodSelect from "../collapse/ApiRequestMethodSelect";
|
||||
import {REQUEST_HEADERS} from "@/common/js/constants";
|
||||
import MsApiVariable from "@/business/components/api/test/components/ApiVariable";
|
||||
import MsJsr233Processor from "../processor/Jsr233Processor";
|
||||
import MsApiAdvancedConfig from "../ApiAdvancedConfig";
|
||||
|
||||
export default {
|
||||
name: "MsApiHttpRequestForm",
|
||||
components: {
|
||||
MsJsr233Processor,
|
||||
MsApiAdvancedConfig,
|
||||
MsApiVariable, ApiRequestMethodSelect, MsApiExtract, MsApiAssertions, MsApiBody, MsApiKeyValue},
|
||||
props: {
|
||||
request: HttpRequest,
|
||||
jsonPathList: Array,
|
||||
scenario: Scenario,
|
||||
isReadOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
let validateURL = (rule, value, callback) => {
|
||||
try {
|
||||
new URL(this.addProtocol(this.request.url));
|
||||
} catch (e) {
|
||||
callback(this.$t('api_test.request.url_invalid'));
|
||||
}
|
||||
};
|
||||
return {
|
||||
activeName: "parameters",
|
||||
rules: {
|
||||
name: [
|
||||
{max: 300, message: this.$t('commons.input_limit', [1, 300]), trigger: 'blur'}
|
||||
],
|
||||
url: [
|
||||
{max: 500, required: true, message: this.$t('commons.input_limit', [1, 500]), trigger: 'blur'},
|
||||
{validator: validateURL, trigger: 'blur'}
|
||||
],
|
||||
path: [
|
||||
{max: 500, message: this.$t('commons.input_limit', [0, 500]), trigger: 'blur'},
|
||||
]
|
||||
},
|
||||
headerSuggestions: REQUEST_HEADERS
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
urlChange() {
|
||||
if (!this.request.url) return;
|
||||
let url = this.getURL(this.addProtocol(this.request.url));
|
||||
if (url) {
|
||||
this.request.url = decodeURIComponent(url.origin + url.pathname);
|
||||
}
|
||||
},
|
||||
pathChange() {
|
||||
if (!this.request.path) return;
|
||||
let url = this.getURL(this.displayUrl);
|
||||
let urlStr = url.origin + url.pathname;
|
||||
let envUrl = this.scenario.environment.config.httpConfig.protocol + '://' + this.scenario.environment.config.httpConfig.socket;
|
||||
this.request.path = decodeURIComponent(urlStr.substring(envUrl.length, urlStr.length));
|
||||
},
|
||||
getURL(urlStr) {
|
||||
try {
|
||||
let url = new URL(urlStr);
|
||||
url.searchParams.forEach((value, key) => {
|
||||
if (key && value) {
|
||||
this.request.parameters.splice(0, 0, new KeyValue({name: key, value: value}));
|
||||
}
|
||||
});
|
||||
return url;
|
||||
} catch (e) {
|
||||
this.$error(this.$t('api_test.request.url_invalid'), 2000);
|
||||
}
|
||||
},
|
||||
methodChange(value) {
|
||||
if (value === 'GET' && this.activeName === 'body') {
|
||||
this.activeName = 'parameters';
|
||||
}
|
||||
},
|
||||
useEnvironmentChange(value) {
|
||||
if (value && !this.scenario.environment) {
|
||||
this.$error(this.$t('api_test.request.please_add_environment_to_scenario'), 2000);
|
||||
this.request.useEnvironment = false;
|
||||
}
|
||||
this.$refs["request"].clearValidate();
|
||||
},
|
||||
addProtocol(url) {
|
||||
if (url) {
|
||||
if (!url.toLowerCase().startsWith("https") && !url.toLowerCase().startsWith("http")) {
|
||||
return "https://" + url;
|
||||
}
|
||||
}
|
||||
return url;
|
||||
},
|
||||
runDebug() {
|
||||
this.$emit('runDebug');
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
displayUrl() {
|
||||
return (this.scenario.environment && this.scenario.environment.config.httpConfig.socket) ?
|
||||
this.scenario.environment.config.httpConfig.protocol + '://' + this.scenario.environment.config.httpConfig.socket + (this.request.path ? this.request.path : '')
|
||||
: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.el-tag {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.environment-display {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.environment-name {
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.adjust-margin-bottom {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.environment-url-tip {
|
||||
color: #F56C6C;
|
||||
}
|
||||
|
||||
.follow-redirects-item {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.do-multipart-post {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
|
||||
<el-form :model="request" :rules="rules" ref="request" label-width="100px" :disabled="isReadOnly">
|
||||
|
||||
<el-form-item :label="$t('api_test.request.name')" prop="name">
|
||||
<el-input :disabled="isReadOnly" v-model="request.name" maxlength="300" show-word-limit/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="!request.useEnvironment" :label="$t('api_test.request.url')" prop="url"
|
||||
class="adjust-margin-bottom">
|
||||
<el-input :disabled="isReadOnly" v-model="request.url" maxlength="500"
|
||||
:placeholder="$t('api_test.request.url_description')" @change="urlChange" clearable>
|
||||
<template v-slot:prepend>
|
||||
<ApiRequestMethodSelect :is-read-only="isReadOnly" :request="request" @change="methodChange"/>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="request.useEnvironment" :label="$t('api_test.request.path')" prop="path">
|
||||
<el-input :disabled="isReadOnly" v-model="request.path" maxlength="500"
|
||||
:placeholder="$t('api_test.request.path_description')" @change="pathChange" clearable>
|
||||
<template v-slot:prepend>
|
||||
<ApiRequestMethodSelect :is-read-only="isReadOnly" :request="request" @change="methodChange"/>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="request.useEnvironment" :label="$t('api_test.request.address')" class="adjust-margin-bottom">
|
||||
<el-tag class="environment-display">
|
||||
<span class="environment-name">{{ scenario.environment ? scenario.environment.name + ': ' : '' }}</span>
|
||||
<span class="environment-url">{{ displayUrl }}</span>
|
||||
<span v-if="!displayUrl"
|
||||
class="environment-url-tip">{{ $t('api_test.request.please_configure_socket_in_environment') }}</span>
|
||||
</el-tag>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-switch
|
||||
v-model="request.useEnvironment"
|
||||
:active-text="$t('api_test.request.refer_to_environment')" @change="useEnvironmentChange">
|
||||
</el-switch>
|
||||
<el-checkbox class="follow-redirects-item" v-model="request.followRedirects">{{$t('api_test.request.follow_redirects')}}</el-checkbox>
|
||||
<el-checkbox class="do-multipart-post" v-model="request.doMultipartPost">{{$t('api_test.request.do_multipart_post')}}</el-checkbox>
|
||||
</el-form-item>
|
||||
|
||||
<el-button :disabled="!request.enable || !scenario.enable || isReadOnly" class="debug-button" size="small"
|
||||
type="primary" @click="runDebug">{{ $t('api_test.request.debug') }}
|
||||
</el-button>
|
||||
|
||||
<el-tabs v-model="activeName">
|
||||
<el-tab-pane :label="$t('api_test.request.parameters')" name="parameters">
|
||||
<ms-api-variable :is-read-only="isReadOnly"
|
||||
:parameters="request.parameters"
|
||||
:environment="scenario.environment"
|
||||
:scenario="scenario"
|
||||
:extract="request.extract"
|
||||
:description="$t('api_test.request.parameters_desc')"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('api_test.request.headers')" name="headers">
|
||||
<ms-api-key-value :is-read-only="isReadOnly" :isShowEnable="true" :suggestions="headerSuggestions" :items="request.headers"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('api_test.request.body')" name="body">
|
||||
<ms-api-body :is-read-only="isReadOnly"
|
||||
:body="request.body"
|
||||
:scenario="scenario"
|
||||
:extract="request.extract"
|
||||
:environment="scenario.environment"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('api_test.request.assertions.label')" name="assertions">
|
||||
<ms-api-assertions :request="request" :is-read-only="isReadOnly" :assertions="request.assertions"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('api_test.request.extract.label')" name="extract">
|
||||
<ms-api-extract :is-read-only="isReadOnly" :extract="request.extract"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('api_test.request.processor.pre_exec_script')" name="jsr223PreProcessor">
|
||||
<ms-jsr233-processor :is-read-only="isReadOnly" :jsr223-processor="request.jsr223PreProcessor"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('api_test.request.processor.post_exec_script')" name="jsr223PostProcessor">
|
||||
<ms-jsr233-processor :is-read-only="isReadOnly" :jsr223-processor="request.jsr223PostProcessor"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('api_test.request.timeout_config')" name="advancedConfig">
|
||||
<ms-api-advanced-config :is-read-only="isReadOnly" :request="request"/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MsApiKeyValue from "../ApiKeyValue";
|
||||
import MsApiBody from "../body/ApiBody";
|
||||
import MsApiAssertions from "../assertion/ApiAssertions";
|
||||
import {HttpRequest, KeyValue, Scenario} from "../../model/ScenarioModel";
|
||||
import MsApiExtract from "../extract/ApiExtract";
|
||||
import ApiRequestMethodSelect from "../collapse/ApiRequestMethodSelect";
|
||||
import {REQUEST_HEADERS} from "@/common/js/constants";
|
||||
import MsApiVariable from "@/business/components/api/test/components/ApiVariable";
|
||||
import MsJsr233Processor from "../processor/Jsr233Processor";
|
||||
import MsApiAdvancedConfig from "../ApiAdvancedConfig";
|
||||
|
||||
export default {
|
||||
name: "MsApiHttpRequestForm",
|
||||
components: {
|
||||
MsJsr233Processor,
|
||||
MsApiAdvancedConfig,
|
||||
MsApiVariable, ApiRequestMethodSelect, MsApiExtract, MsApiAssertions, MsApiBody, MsApiKeyValue},
|
||||
props: {
|
||||
request: HttpRequest,
|
||||
jsonPathList: Array,
|
||||
scenario: Scenario,
|
||||
isReadOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
let validateURL = (rule, value, callback) => {
|
||||
try {
|
||||
new URL(this.addProtocol(this.request.url));
|
||||
} catch (e) {
|
||||
callback(this.$t('api_test.request.url_invalid'));
|
||||
}
|
||||
};
|
||||
return {
|
||||
activeName: "parameters",
|
||||
rules: {
|
||||
name: [
|
||||
{max: 300, message: this.$t('commons.input_limit', [1, 300]), trigger: 'blur'}
|
||||
],
|
||||
url: [
|
||||
{max: 500, required: true, message: this.$t('commons.input_limit', [1, 500]), trigger: 'blur'},
|
||||
{validator: validateURL, trigger: 'blur'}
|
||||
],
|
||||
path: [
|
||||
{max: 500, message: this.$t('commons.input_limit', [0, 500]), trigger: 'blur'},
|
||||
]
|
||||
},
|
||||
headerSuggestions: REQUEST_HEADERS
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
urlChange() {
|
||||
if (!this.request.url) return;
|
||||
let url = this.getURL(this.addProtocol(this.request.url));
|
||||
if (url) {
|
||||
this.request.url = decodeURIComponent(url.origin + url.pathname);
|
||||
}
|
||||
},
|
||||
pathChange() {
|
||||
if (!this.request.path) return;
|
||||
let url = this.getURL(this.displayUrl);
|
||||
let urlStr = url.origin + url.pathname;
|
||||
let envUrl = this.scenario.environment.config.httpConfig.protocol + '://' + this.scenario.environment.config.httpConfig.socket;
|
||||
this.request.path = decodeURIComponent(urlStr.substring(envUrl.length, urlStr.length));
|
||||
},
|
||||
getURL(urlStr) {
|
||||
try {
|
||||
let url = new URL(urlStr);
|
||||
url.searchParams.forEach((value, key) => {
|
||||
if (key && value) {
|
||||
this.request.parameters.splice(0, 0, new KeyValue({name: key, value: value}));
|
||||
}
|
||||
});
|
||||
return url;
|
||||
} catch (e) {
|
||||
this.$error(this.$t('api_test.request.url_invalid'), 2000);
|
||||
}
|
||||
},
|
||||
methodChange(value) {
|
||||
if (value === 'GET' && this.activeName === 'body') {
|
||||
this.activeName = 'parameters';
|
||||
}
|
||||
},
|
||||
useEnvironmentChange(value) {
|
||||
if (value && !this.scenario.environment) {
|
||||
this.$error(this.$t('api_test.request.please_add_environment_to_scenario'), 2000);
|
||||
this.request.useEnvironment = false;
|
||||
}
|
||||
this.$refs["request"].clearValidate();
|
||||
},
|
||||
addProtocol(url) {
|
||||
if (url) {
|
||||
if (!url.toLowerCase().startsWith("https") && !url.toLowerCase().startsWith("http")) {
|
||||
return "https://" + url;
|
||||
}
|
||||
}
|
||||
return url;
|
||||
},
|
||||
runDebug() {
|
||||
this.$emit('runDebug');
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
displayUrl() {
|
||||
return (this.scenario.environment && this.scenario.environment.config.httpConfig.socket) ?
|
||||
this.scenario.environment.config.httpConfig.protocol + '://' + this.scenario.environment.config.httpConfig.socket + (this.request.path ? this.request.path : '')
|
||||
: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.el-tag {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.environment-display {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.environment-name {
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.adjust-margin-bottom {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.environment-url-tip {
|
||||
color: #F56C6C;
|
||||
}
|
||||
|
||||
.follow-redirects-item {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.do-multipart-post {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="request-form">
|
||||
<component @runDebug="runDebug" :is="component" :jsonPathList="jsonPathList" :is-read-only="isReadOnly" :request="request" :scenario="scenario"/>
|
||||
<component @runDebug="runDebug" :is="component" :is-read-only="isReadOnly" :request="request" :scenario="scenario"/>
|
||||
<el-divider v-if="isCompleted"></el-divider>
|
||||
<ms-request-result-tail v-loading="debugReportLoading" v-if="isCompleted" :request="request.debugRequestResult ? request.debugRequestResult : {responseResult: {}, subRequestResults: []}"
|
||||
:scenario-name="request.debugScenario ? request.debugScenario.name : ''" ref="msDebugResult"/>
|
||||
@ -96,37 +96,6 @@ export default {
|
||||
if (res.scenarios && res.scenarios.length > 0) {
|
||||
this.request.debugScenario = res.scenarios[0];
|
||||
this.request.debugRequestResult = this.request.debugScenario.requestResults[0];
|
||||
|
||||
//add by Cuipeng
|
||||
this.debugResultDetails=this.request.debugRequestResult.responseResult.body;
|
||||
// console.log(this.debugResultDetails);
|
||||
try {
|
||||
|
||||
let param = {
|
||||
jsonPath: this.debugResultDetails
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
this.$post("/api/getJsonPaths", param).then(response1 => {
|
||||
|
||||
this.jsonPathList = response1.data.data;
|
||||
// console.log(this.jsonPathList);
|
||||
|
||||
}).catch(() => {
|
||||
|
||||
this.$warning("获取推荐jsonpath列表失败");
|
||||
});
|
||||
|
||||
|
||||
|
||||
} catch (e) {
|
||||
alert("调试结果的返回不是一个json");
|
||||
throw e;
|
||||
}
|
||||
//add by Cuipeng
|
||||
|
||||
this.deleteReport(this.debugReportId);
|
||||
} else {
|
||||
this.request.debugScenario = new Scenario();
|
||||
|
@ -557,7 +557,12 @@ export default {
|
||||
expect: "Expect Value",
|
||||
expression: "Expression",
|
||||
response_in_time: "Response in time",
|
||||
ignore_status: "Ignore Status"
|
||||
ignore_status: "Ignore Status",
|
||||
json_path_add: "Add JONPATH Assertions",
|
||||
json_path_err: "The response result is not in JSON format",
|
||||
json_path_suggest: "JSONPath Assertion Suggest",
|
||||
json_path_clear: "Clear JSONPath Assertion",
|
||||
debug_first: "First, debug to get the response",
|
||||
},
|
||||
extract: {
|
||||
label: "Extract from response",
|
||||
|
@ -557,7 +557,11 @@ export default {
|
||||
expect: "期望值",
|
||||
expression: "Perl型正则表达式",
|
||||
response_in_time: "响应时间在...毫秒以内",
|
||||
ignore_status: "忽略状态"
|
||||
json_path_add: "添加 JONPATH 断言",
|
||||
json_path_err: "响应结果不是 JSON 格式",
|
||||
json_path_suggest: "推荐JSONPath断言",
|
||||
json_path_clear: "清空JSONPath断言",
|
||||
debug_first: "请先执行调试获取响应结果",
|
||||
},
|
||||
extract: {
|
||||
label: "提取",
|
||||
|
@ -217,20 +217,25 @@ export default {
|
||||
delete_warning: '刪除該組織將同步刪除該組織下所有相關工作空間和相關工作空間下的所有項目,以及項目中的所有用例、接口測試、性能測試等,確定要刪除嗎?',
|
||||
service_integration: '服務集成',
|
||||
defect_manage: '缺陷管理平臺',
|
||||
message_settings:'消息設定',
|
||||
message_settings:'消息設置',
|
||||
message:{
|
||||
jenkins_task_notification: 'Jenkins任務通知',
|
||||
test_plan_task_notification: '測試計畫任務通知',
|
||||
jenkins_task_notification: 'Jenkins接口調用任務通知',
|
||||
test_plan_task_notification: '測試計劃任務通知',
|
||||
test_review_task_notice: '測試評審任務通知',
|
||||
defect_task_notification: '缺陷任務通知',
|
||||
create_new_notification: '創建新通知',
|
||||
select_events: '選擇事件',
|
||||
select_receiving_method: '選擇接收管道',
|
||||
defect_task_notification: '缺陷任務通知',
|
||||
select_receiving_method: '選擇接收方式',
|
||||
mail: '郵件',
|
||||
nail_robot: '釘釘機器人',
|
||||
enterprise_wechat_robot: '企業微信機器人',
|
||||
message_webhook: '接收管道為釘釘和企業機器人時,webhook為必填項\n' +
|
||||
'\n'
|
||||
notes: '註意: 1.事件,接收方式,接收人為必填項;\n' +
|
||||
' 2.接收方式除郵件外webhook為必填;\n' +
|
||||
' 3.機器人選擇為群機器人,安全驗證選擇“自定義關鍵詞” :"任務通知"',
|
||||
message: '事件,接收人,接收方式為必填項',
|
||||
message_webhook: '接收方式為釘釘和企業機器人時,webhook為必填項'
|
||||
|
||||
|
||||
},
|
||||
integration: {
|
||||
select_defect_platform: '請選擇要集成的缺陷管理平臺:',
|
||||
@ -254,17 +259,7 @@ export default {
|
||||
successful_operation: '操作成功',
|
||||
not_integrated: '未集成該平臺',
|
||||
choose_platform: '請選擇集成的平臺',
|
||||
verified: '驗證通過',
|
||||
mail: '郵件',
|
||||
nail_robot: '釘釘機器人',
|
||||
enterprise_wechat_robot: '企業微信機器人',
|
||||
notes: '注意:1.事件,接收管道,接收人為必填項;\n' +
|
||||
'\n' +
|
||||
'2.接收管道除郵件外webhook為必填;\n' +
|
||||
'\n' +
|
||||
'3.機器人選擇為群機器人,安全驗證選擇“自定義關鍵字”:“任務通知”',
|
||||
message: '事件,接收人,接收管道為必填項\n' +
|
||||
'\n'
|
||||
verified: '驗證通過'
|
||||
}
|
||||
},
|
||||
project: {
|
||||
@ -562,7 +557,11 @@ export default {
|
||||
expect: "期望值",
|
||||
expression: "Perl型正則表達式",
|
||||
response_in_time: "響應時間在...毫秒以內",
|
||||
ignore_status: "忽略狀態"
|
||||
json_path_add: "添加 JONPATH 斷言",
|
||||
json_path_err: "響應結果不是 JSON 格式",
|
||||
json_path_suggest: "推薦JSONPath斷言",
|
||||
json_path_clear: "清空JSONPath斷言",
|
||||
debug_first: "請先執行調試獲取響應結果",
|
||||
},
|
||||
extract: {
|
||||
label: "提取",
|
||||
|
Loading…
Reference in New Issue
Block a user