fix(接口测试): 修复断言相关bug

This commit is contained in:
wxg0103 2024-03-21 21:32:18 +08:00 committed by Craftsman
parent 2a43f3fc44
commit 281abfcfcc
16 changed files with 190 additions and 92 deletions

View File

@ -1,6 +1,8 @@
package io.metersphere.api.dto.request.controller;
import io.metersphere.plugin.api.spi.AbstractMsTestElement;
import io.metersphere.sdk.constants.MsAssertionCondition;
import io.metersphere.system.valid.EnumValue;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.lang3.StringUtils;
@ -15,9 +17,8 @@ public class MsIfController extends AbstractMsTestElement {
* 变量名称 ${variable} 长度255
*/
private String variable;
/**
* 操作符 == ,!=, < ,<=, >, >=, contains (=~),not contains (!~), is empty, is not empty
*/
@EnumValue(enumClass = io.metersphere.sdk.constants.MsAssertionCondition.class)
private String condition;
/**
* ${value} 长度255
@ -25,7 +26,7 @@ public class MsIfController extends AbstractMsTestElement {
private String value;
public boolean isValid() {
if (StringUtils.contains(condition, "is empty")) {
if (StringUtils.contains(condition, MsAssertionCondition.EMPTY.name())) {
return StringUtils.isNotBlank(variable);
}
return StringUtils.isNotBlank(variable) && StringUtils.isNotBlank(condition) && StringUtils.isNotBlank(value);
@ -61,35 +62,33 @@ public class MsIfController extends AbstractMsTestElement {
: StringUtils.join("vars.get('", key, "')");
String operator = this.condition;
String value;
switch (operator) {
case "is empty":
variable = variable + "==" + "\"\\" + this.variable + "\"" + "|| empty(" + variable + ")";
operator = "";
value = "";
break;
case "is not empty":
variable = variable + "!=" + "\"\\" + this.variable + "\"" + "&& !empty(" + variable + ")";
operator = "";
value = "";
break;
case "<":
case ">":
case "<=":
case ">=":
value = this.value;
break;
case "=~":
case "!~":
value = "\"(\\n|.)*" + this.value + "(\\n|.)*\"";
break;
default:
value = "\"" + this.value + "\"";
break;
}
MsAssertionCondition msAssertionCondition = MsAssertionCondition.valueOf(operator);
return switch (msAssertionCondition) {
case EMPTY ->
buildExpression(variable + "==" + "\"\\" + this.variable + "\"" + "|| empty(" + variable + ")");
case NOT_EMPTY ->
buildExpression(variable + "!=" + "\"\\" + this.variable + "\"" + "&& !empty(" + variable + ")");
case GT ->
buildExpression(StringUtils.isNumeric(value) ? variable + ">" + value : variable + ">" + "\"" + value + "\"");
case LT ->
buildExpression(StringUtils.isNumeric(value) ? variable + "<" + value : variable + "<" + "\"" + value + "\"");
case GT_OR_EQUALS ->
buildExpression(StringUtils.isNumeric(value) ? variable + ">=" + value : variable + ">=" + "\"" + value + "\"");
case LT_OR_EQUALS ->
buildExpression(StringUtils.isNumeric(value) ? variable + "<=" + value : variable + "<=" + "\"" + value + "\"");
case CONTAINS -> buildExpression("\"(\\n|.)*" + value + "(\\n|.)*\"=~" + variable);
case NOT_CONTAINS -> buildExpression("\"(\\n|.)*" + value + "(\\n|.)*\"!~" + variable);
case EQUALS ->
buildExpression(StringUtils.isNumeric(value) ? variable + "==" + value : variable + "==" + "\"" + value + "\"");
case NOT_EQUALS ->
buildExpression(StringUtils.isNumeric(value) ? variable + "!=" + value : variable + "!=" + "\"" + value + "\"");
default -> buildExpression("\"" + condition + value + "\"");
};
}
return "${__jexl3(" + variable + operator + value + ")}";
private String buildExpression(String expression) {
return "${__jexl3(" + expression + ")}";
}
}

View File

@ -1,5 +1,7 @@
package io.metersphere.api.dto.request.controller.loop;
import io.metersphere.sdk.constants.MsAssertionCondition;
import io.metersphere.system.valid.EnumValue;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
@ -10,8 +12,9 @@ public class MsWhileVariable {
*/
private String variable;
/**
* 操作符 == ,!=, < ,<=, >, >=, contains (=~),not contains (!~), is empty, is not empty
* 操作符
*/
@EnumValue(enumClass = io.metersphere.sdk.constants.MsAssertionCondition.class)
private String condition;
/**
* 255
@ -20,37 +23,26 @@ public class MsWhileVariable {
public String getConditionValue() {
String variable = "\"" + this.getVariable() + "\"";
String operator = this.getCondition();
String value = null;
switch (operator) {
case "is empty":
variable = "(" + variable + "==" + "\"\\" + this.getVariable() + "\"" + "|| empty(" + variable + "))";
operator = "";
break;
case "is not empty":
variable = "(" + variable + "!=" + "\"\\" + this.getVariable() + "\"" + "&& !empty(" + variable + "))";
operator = "";
break;
case "<":
case ">":
case "<=":
case ">=":
if (StringUtils.isNumeric(this.getValue())) {
value = this.getValue();
} else {
value = "\"" + this.getValue() + "\"";
}
break;
case "=~":
case "!~":
value = "\"(\\n|.)*" + this.getVariable() + "(\\n|.)*\"";
break;
default:
value = "\"" + this.getValue() + "\"";
break;
}
return variable + operator + value;
String value = this.getValue();
MsAssertionCondition msAssertionCondition = MsAssertionCondition.valueOf(this.getCondition());
return switch (msAssertionCondition) {
case EMPTY -> String.format("(%s==\"\\\"%s\\\"\"|| empty(%s))", variable, this.getVariable(), variable);
case NOT_EMPTY ->
String.format("(%s!=\"\\\"%s\\\"\"&& !empty(%s))", variable, this.getVariable(), variable);
case GT -> StringUtils.isNumeric(value) ? variable + ">" + value : variable + ">\"" + value + "\"";
case LT -> StringUtils.isNumeric(value) ? variable + "<" + value : variable + "<\"" + value + "\"";
case LT_OR_EQUALS ->
StringUtils.isNumeric(value) ? variable + "<=" + value : variable + "<=\"" + value + "\"";
case GT_OR_EQUALS ->
StringUtils.isNumeric(value) ? variable + ">=" + value : variable + ">=\"" + value + "\"";
case CONTAINS -> String.format("\"(\\n|.)*%s(\\n|.)*\"=~", variable);
case NOT_CONTAINS -> String.format("\"(\\n|.)*%s(\\n|.)*\"!~", variable);
case EQUALS ->
StringUtils.isNumeric(value) ? variable + "==" + value : variable + "==" + "\"" + value + "\"";
case NOT_EQUALS ->
StringUtils.isNumeric(value) ? variable + "!=" + value : variable + "!=" + "\"" + value + "\"";
default -> "\"" + value + "\"";
};
}
}

View File

@ -325,7 +325,7 @@ public class MsHTTPElementConverter extends AbstractJmeterElementConverter<MsHTT
headerManager.setName(StringUtils.isNotEmpty(msHTTPElement.getName()) ? msHTTPElement.getName() + "_HeaderManager" : "HeaderManager");
headerManager.setProperty(TestElement.TEST_CLASS, HeaderManager.class.getName());
headerManager.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass(HEADER_PANEL));
headerMap.forEach((k, v) -> headerManager.add(new Header(k, Mock.buildFunctionCallString(v))));
headerMap.forEach((k, v) -> headerManager.add(new org.apache.jmeter.protocol.http.control.Header(k, Mock.buildFunctionCallString(v))));
return headerManager;
}

View File

@ -55,7 +55,7 @@ public class VariableAssertionConverter extends AssertionConverter<MsVariableAss
String name = String.format("Variable '%s' expect %s %s", variableName, condition.toLowerCase().replace("_", ""), expectedValue);
scriptProcessor.setName(name);
scriptProcessor.setScriptLanguage(ScriptLanguageType.BEANSHELL_JSR233.name());
scriptProcessor.setScriptLanguage(ScriptLanguageType.GROOVY.name());
JSR223Assertion jsr223Assertion = new JSR223Assertion();
ScriptProcessorConverter.parse(jsr223Assertion, scriptProcessor);
return jsr223Assertion;
@ -77,7 +77,7 @@ public class VariableAssertionConverter extends AssertionConverter<MsVariableAss
handleMap.put(MsAssertionCondition.EQUALS.name(),
"""
result = expectation.equals(variableValue);"
result = expectation.equals(variableValue);
msg = "value == " + expectation;
""");
@ -190,8 +190,10 @@ public class VariableAssertionConverter extends AssertionConverter<MsVariableAss
String condition = variableAssertionItem.getCondition();
String handleScript = handleMap.get(condition);
if (StringUtils.isNotBlank(handleScript)) {
if (StringUtils.isBlank(handleScript)) {
script += handleMap.get(MsAssertionCondition.EQUALS.name());
} else {
script += handleScript;
}
script += """

View File

@ -922,7 +922,7 @@ public class ApiScenarioControllerTests extends BaseTest {
public MsIfController ifController(String name, boolean enable) {
//条件控制器
MsIfController msIfController = new MsIfController();
msIfController.setCondition("==");
msIfController.setCondition(MsAssertionCondition.EQUALS.name());
msIfController.setName(StringUtils.isNotBlank(name) ? name : "条件控制器");
msIfController.setEnable(enable);
msIfController.setVariable("1");
@ -972,7 +972,7 @@ public class ApiScenarioControllerTests extends BaseTest {
if (StringUtils.isNotBlank(condition) && StringUtils.equals(condition, "CONDITION")) {
MsWhileVariable msWhileVariable = new MsWhileVariable();
msWhileVariable.setVariable("1");
msWhileVariable.setCondition("==");
msWhileVariable.setCondition(MsAssertionCondition.EQUALS.name());
msWhileVariable.setValue("1");
whileController.setMsWhileVariable(msWhileVariable);
} else {

View File

@ -93,5 +93,9 @@ public class MsDocumentAssertionElement {
return value;
}
}
public String getJsonPath() {
//TODO 未实现
return jsonPath;
}
}

View File

@ -7,6 +7,7 @@ import io.metersphere.project.domain.ProjectExample;
import io.metersphere.project.dto.MoveNodeSortDTO;
import io.metersphere.project.dto.environment.*;
import io.metersphere.project.dto.environment.datasource.DataSource;
import io.metersphere.project.dto.environment.http.HttpConfig;
import io.metersphere.project.mapper.ExtEnvironmentMapper;
import io.metersphere.project.mapper.ProjectMapper;
import io.metersphere.sdk.constants.DefaultRepositoryDir;
@ -209,10 +210,21 @@ public class EnvironmentService extends MoveNodeService {
Project project = projectMapper.selectByPrimaryKey(environment.getProjectId());
String domain = baseUrl.replace(HTTP, StringUtils.EMPTY).replace(HTTPS, StringUtils.EMPTY);
String protocol = baseUrl.substring(0, baseUrl.indexOf(domain) -3 );
environmentInfoDTO.getConfig().getHttpConfig().getFirst().setId(IDGenerator.nextStr());
environmentInfoDTO.getConfig().getHttpConfig().getFirst().setProtocol(protocol);
environmentInfoDTO.getConfig().getHttpConfig().getFirst().setHostname(StringUtils.join(domain, MOCK_EVN_SOCKET, project.getNum()));
environmentInfoDTO.getConfig().getHttpConfig().getFirst().setUrl(StringUtils.join(baseUrl, MOCK_EVN_SOCKET, project.getNum()));
if (environmentInfoDTO.getConfig() != null && CollectionUtils.isNotEmpty(environmentInfoDTO.getConfig().getHttpConfig())) {
environmentInfoDTO.getConfig().getHttpConfig().getFirst().setId(IDGenerator.nextStr());
environmentInfoDTO.getConfig().getHttpConfig().getFirst().setProtocol(protocol);
environmentInfoDTO.getConfig().getHttpConfig().getFirst().setHostname(StringUtils.join(domain, MOCK_EVN_SOCKET, project.getNum()));
environmentInfoDTO.getConfig().getHttpConfig().getFirst().setUrl(StringUtils.join(baseUrl, MOCK_EVN_SOCKET, project.getNum()));
} else {
List<HttpConfig> httpConfigs = new ArrayList<>();
HttpConfig httpConfig = new HttpConfig();
httpConfig.setId(IDGenerator.nextStr());
httpConfig.setProtocol(protocol);
httpConfig.setHostname(StringUtils.join(domain, MOCK_EVN_SOCKET, project.getNum()));
httpConfig.setUrl(StringUtils.join(baseUrl, MOCK_EVN_SOCKET, project.getNum()));
httpConfigs.add(httpConfig);
environmentInfoDTO.getConfig().setHttpConfig(httpConfigs);
}
}
}
}

View File

@ -817,6 +817,20 @@ public class EnvironmentControllerTests extends BaseTest {
response = parseObjectFromMvcResult(mvcResult, EnvironmentInfoDTO.class);
Assertions.assertNotNull(response);
Assertions.assertEquals("environmentId1", response.getId());
//插入一个为空的mock环境
Environment mockEnvironment = new Environment();
mockEnvironment.setId("mock-null");
mockEnvironment.setMock(true);
mockEnvironment.setProjectId(DEFAULT_PROJECT_ID);
mockEnvironment.setUpdateUser("updateUser");
mockEnvironment.setUpdateTime(System.currentTimeMillis());
mockEnvironment.setCreateUser("createUser");
mockEnvironment.setPos(1000L);
mockEnvironment.setName("mock-null");
mockEnvironment.setCreateTime(System.currentTimeMillis());
environmentMapper.insert(mockEnvironment);
this.responseGet(get + "mock-null");
environmentMapper.deleteByPrimaryKey("mock-null");
//校验权限
requestGetPermissionTest(PROJECT_ENVIRONMENT_READ, get + DEFAULT_PROJECT_ID);
EnvironmentExample environmentExample = new EnvironmentExample();

View File

@ -99,7 +99,7 @@
:columns="xPathColumns"
:scroll="{ minWidth: '700px' }"
:default-param-item="xPathDefaultParamItem"
@change="(data) => handleChange(data, ResponseBodyAssertionType.XPATH)"
@change="(data:any[],isInit?: boolean) => handleChange(data, ResponseBodyAssertionType.XPATH,isInit)"
@more-action-select="(e,r)=> handleExtractParamMoreActionSelect(e,r as ExpressionConfig)"
>
<template #expression="{ record, rowIndex }">
@ -136,7 +136,7 @@
:class="
disabledExpressionSuffix ? 'ms-params-input-suffix-icon--disabled' : 'ms-params-input-suffix-icon'
"
@click.stop="() => showFastExtraction(record, RequestExtractExpressionEnum.JSON_PATH)"
@click.stop="() => showFastExtraction(record, RequestExtractExpressionEnum.X_PATH)"
/>
</a-tooltip>
</template>
@ -276,7 +276,7 @@
:class="
disabledExpressionSuffix ? 'ms-params-input-suffix-icon--disabled' : 'ms-params-input-suffix-icon'
"
@click.stop="() => showFastExtraction(record, RequestExtractExpressionEnum.JSON_PATH)"
@click.stop="() => showFastExtraction(record, RequestExtractExpressionEnum.REGEX)"
/>
</a-tooltip>
</template>
@ -313,7 +313,12 @@
<conditionContent v-model:data="condition.script" :height-used="600" is-build-in class="mt-[16px]" />
</div> -->
</div>
<fastExtraction v-model:visible="fastExtractionVisible" :config="activeRecord" @apply="handleFastExtractionApply" />
<fastExtraction
v-model:visible="fastExtractionVisible"
:config="activeRecord"
:response="props.response"
@apply="handleFastExtractionApply"
/>
</template>
<script setup lang="ts">
@ -373,6 +378,7 @@
const props = defineProps<{
data: Param;
response?: string;
}>();
const activeTab = ref(ResponseBodyAssertionType.JSON_PATH);
const activeResponseFormat = ref('XML');
@ -500,10 +506,8 @@
condition.value.xpathAssertion.assertions = data;
if (!isInit) {
emit('change', {
...defaultParamItem,
...condition.value,
assertionBodyType: activeTab.value,
responseFormat: activeResponseFormat.value,
});
}
break;
@ -655,6 +659,25 @@
}
return e;
});
condition.value.xpathAssertion.assertions = condition.value.xpathAssertion.assertions?.map((e) => {
if (e.id === activeRecord.value.id) {
return {
...e,
...config,
};
}
return e;
});
condition.value.regexAssertion.assertions = condition.value.regexAssertion.assertions?.map((e) => {
if (e.id === activeRecord.value.id) {
return {
...e,
...config,
};
}
return e;
});
fastExtractionVisible.value = false;
nextTick(() => {
if (activeTab.value === ResponseBodyAssertionType.JSON_PATH) {

View File

@ -47,10 +47,11 @@
{
title: 'ms.assertion.responseHeader', //
dataIndex: 'header',
slotName: 'header',
slotName: 'key',
showInTable: true,
showDrag: true,
options: responseHeaderOption,
inputType: 'autoComplete',
autoCompleteParams: responseHeaderOption,
},
{
title: 'ms.assertion.matchCondition', //

View File

@ -7,6 +7,7 @@
<a-input-number
v-model="condition.expectedValue"
:step="100"
:min="0"
mode="button"
@blur="
emit('change', {

View File

@ -93,6 +93,7 @@
<ResponseBodyTab
v-if="valueKey === ResponseAssertionType.RESPONSE_BODY"
v-model:data="getCurrentItemState"
:response="props.response"
@change="handleChange"
/>
<!-- 响应时间 -->
@ -157,6 +158,7 @@
const props = defineProps<{
isDefinition?: boolean; //
assertionConfig?: ExecuteAssertionConfig; //
response?: string; //
}>();
const emit = defineEmits<{
@ -296,8 +298,8 @@
case ResponseAssertionType.RESPONSE_CODE:
assertions.value.push({
...tmpObj,
condition: '',
expectedValue: '',
condition: 'EQUALS',
expectedValue: '200',
});
break;
case ResponseAssertionType.RESPONSE_BODY:
@ -321,7 +323,7 @@
case ResponseAssertionType.RESPONSE_TIME:
assertions.value.push({
...tmpObj,
expectedValue: 0,
expectedValue: 100,
});
break;
case ResponseAssertionType.VARIABLE:

View File

@ -5,14 +5,27 @@ export { default as MsAdvanceFilter } from './index.vue';
// const NOT_IN = { label: 'advanceFilter.operator.not_in', value: 'not_in' };
const LIKE = { label: 'advanceFilter.operator.like', value: 'like' };
const NOT_LIKE = { label: 'advanceFilter.operator.not_like', value: 'not_like' };
const GT = { label: 'advanceFilter.operator.gt', value: 'gt' };
const GE = { label: 'advanceFilter.operator.ge', value: 'ge' };
const LT = { label: 'advanceFilter.operator.lt', value: 'lt' };
const LE = { label: 'advanceFilter.operator.le', value: 'le' };
const EQUAL = { label: 'advanceFilter.operator.equal', value: 'equal' };
const NOT_EQUAL = { label: 'advanceFilter.operator.notEqual', value: 'not_equal' };
const GT = { label: 'advanceFilter.operator.gt', value: 'GT' };
const GE = { label: 'advanceFilter.operator.ge', value: 'GT_OR_EQUALS' };
const LT = { label: 'advanceFilter.operator.lt', value: 'LT' };
const LE = { label: 'advanceFilter.operator.le', value: 'LT_OR_EQUALS' };
const EQUAL = { label: 'advanceFilter.operator.equal', value: 'EQUALS' };
const NOT_EQUAL = { label: 'advanceFilter.operator.notEqual', value: 'NOT_EQUALS' };
const BETWEEN = { label: 'advanceFilter.operator.between', value: 'between' };
const NO_CHECK = { label: 'advanceFilter.operator.no_check', value: 'UNCHECK' };
const CONTAINS = { label: 'advanceFilter.operator.contains', value: 'CONTAINS' };
const NO_CONTAINS = { label: 'advanceFilter.operator.not_contains', value: 'NOT_CONTAINS' };
const START_WITH = { label: 'advanceFilter.operator.start_with', value: 'START_WITH' };
const END_WITH = { label: 'advanceFilter.operator.end_with', value: 'END_WITH' };
const EMPTY = { label: 'advanceFilter.operator.empty', value: 'EMPTY' };
const NOT_EMPTY = { label: 'advanceFilter.operator.not_empty', value: 'NOT_EMPTY' };
const REGEX = { label: 'advanceFilter.operator.regexp', value: 'REGEX' };
const LENGTH_EQUAL = { label: 'advanceFilter.operator.length.equal', value: 'LENGTH_EQUALS' };
const LENGTH_NOT_EQUAL = { label: 'advanceFilter.operator.length.not_equal', value: 'LENGTH_NOT_EQUALS' };
const LENGTH_GT = { label: 'advanceFilter.operator.length.gt', value: 'LENGTH_GT' };
const LENGTH_GE = { label: 'advanceFilter.operator.length.ge', value: 'LENGTH_GT_OR_EQUALS' };
const LENGTH_LT = { label: 'advanceFilter.operator.length.lt', value: 'LENGTH_LT' };
const LENGTH_LE = { label: 'advanceFilter.operator.length.le', value: 'LENGTH_LT_OR_EQUALS' };
export const OPERATOR_MAP = {
string: [LIKE, NOT_LIKE, EQUAL, NOT_EQUAL],
number: [GT, GE, LT, LE, EQUAL, NOT_EQUAL, BETWEEN],
@ -23,13 +36,25 @@ export const OPERATOR_MAP = {
export const timeSelectOptions = [GE, LE];
export const statusCodeOptions = [
{ label: 'ms.assertion.noValidation', value: 'none' },
NO_CHECK,
EQUAL,
NOT_EQUAL,
GT,
GE,
LT,
LE,
CONTAINS,
NO_CONTAINS,
START_WITH,
END_WITH,
EMPTY,
NOT_EMPTY,
REGEX,
LENGTH_GT,
LENGTH_GE,
LENGTH_LT,
LENGTH_LE,
LENGTH_EQUAL,
];
export const CustomTypeMaps: Record<string, any> = {

View File

@ -18,4 +18,12 @@ export default {
'advanceFilter.reset': 'Reset',
'advanceFilter.filter': 'Filter',
'advanceFilter.plaseInputFilterContent': 'Please input filter content',
'advanceFilter.operator.no_check': 'No check',
'advanceFilter.operator.contains': 'Contains',
'advanceFilter.operator.not_contains': 'Not contains',
'advanceFilter.operator.start_with': 'Start with',
'advanceFilter.operator.end_with': 'End with',
'advanceFilter.operator.empty': 'Empty',
'advanceFilter.operator.not_empty': 'Not empty',
'advanceFilter.operator.regexp': 'Regexp',
};

View File

@ -10,6 +10,14 @@ export default {
'advanceFilter.operator.equal': '等于',
'advanceFilter.operator.notEqual': '不等于',
'advanceFilter.operator.between': '介于',
'advanceFilter.operator.no_check': '不校验',
'advanceFilter.operator.contains': '包含',
'advanceFilter.operator.not_contains': '不包含',
'advanceFilter.operator.start_with': '以...开始',
'advanceFilter.operator.end_with': '以...结束',
'advanceFilter.operator.empty': '为空',
'advanceFilter.operator.not_empty': '不为空',
'advanceFilter.operator.regexp': '正则匹配',
'advanceFilter.setFilterCondition': '设置筛选条件',
'advanceFilter.accordBelow': '满足以下',
'advanceFilter.all': '所有',
@ -21,4 +29,10 @@ export default {
'advanceFilter.plaseSelectFilterDataIndex': '请选择过滤条件',
'advanceFilter.plaseInputFilterContent': '请输入筛选内容',
'advanceFilter.plaseSelectOperator': '请选择运算符',
'advanceFilter.operator.length.equal': '长度等于',
'advanceFilter.operator.length.not_equal': '长度不等于',
'advanceFilter.operator.length.gt': '长度大于',
'advanceFilter.operator.length.ge': '长度大于等于',
'advanceFilter.operator.length.lt': '长度小于',
'advanceFilter.operator.length.le': '长度小于等于',
};

View File

@ -248,6 +248,7 @@
v-else-if="requestVModel.activeTab === RequestComposition.ASSERTION"
v-model:params="requestVModel.children[0].assertionConfig.assertions"
:is-definition="props.isDefinition"
:response="requestVModel.response?.requestResults[0]?.responseResult.body"
:assertion-config="requestVModel.children[0].assertionConfig"
/>
<auth