fix(接口测试): json-schema按照正则表达式自动生成数据报错

--bug=1044111 --user=陈建星 【接口测试】接口-json请求参数-schema格式-string类型字段-参数值为空-json自动生成-正则表达式获取失败 https://www.tapd.cn/55049933/s/1550058
This commit is contained in:
AgAngle 2024-07-17 18:14:38 +08:00 committed by Craftsman
parent 0480e75c43
commit 3dbde4f287
13 changed files with 630 additions and 15 deletions

View File

@ -94,12 +94,6 @@
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<!-- 根据正则表达式,生成随机字符串 -->
<dependency>
<groupId>com.github.krraghavan</groupId>
<artifactId>xeger</artifactId>
<version>${xeger.version}</version>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -2,14 +2,16 @@ package io.metersphere.api.utils;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.*; import com.fasterxml.jackson.databind.node.*;
import io.metersphere.api.utils.regex.model.Node;
import io.metersphere.api.utils.regex.model.OrdinaryNode;
import io.metersphere.project.constants.PropertyConstant; import io.metersphere.project.constants.PropertyConstant;
import nl.flotsam.xeger.Xeger;
import org.apache.commons.collections4.MapUtils; import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.RandomStringGenerator; import org.apache.commons.text.RandomStringGenerator;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.security.SecureRandom;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Random; import java.util.Random;
@ -143,13 +145,17 @@ public class JsonSchemaBuilder {
int max = isTextNotBlank(maxLength) ? maxLength.asInt() : 20; int max = isTextNotBlank(maxLength) ? maxLength.asInt() : 20;
int min = isTextNotBlank(minLength) ? minLength.asInt() : 1; int min = isTextNotBlank(minLength) ? minLength.asInt() : 1;
if (enumValues != null && enumValues instanceof ArrayNode) { if (enumValues != null && enumValues instanceof ArrayNode) {
value = enumValues.get(new Random().nextInt(enumValues.size())).asText(); value = enumValues.get(new SecureRandom().nextInt(enumValues.size())).asText();
if (value.length() > max) { if (value.length() > max) {
value = value.substring(0, max); value = value.substring(0, max);
} }
} else if (isTextNotBlank(pattern)) { } else if (isTextNotBlank(pattern)) {
Xeger generator = new Xeger(pattern.asText()); try {
value = generator.generate(); Node node = new OrdinaryNode(pattern.asText());
value = node.random();
} catch (Exception e) {
value = pattern.asText();
}
} else if (isTextNotBlank(defaultValue)) { } else if (isTextNotBlank(defaultValue)) {
value = defaultValue.asText(); value = defaultValue.asText();
if (value.length() > max) { if (value.length() > max) {
@ -159,7 +165,7 @@ public class JsonSchemaBuilder {
value = value + generateStr(min - value.length()); value = value + generateStr(min - value.length());
} }
} else { } else {
value = generateStr(new Random().nextInt(max - min + 1) + min); value = generateStr(new SecureRandom().nextInt(max - min + 1) + min);
} }
} }
yield new TextNode(value); yield new TextNode(value);
@ -169,7 +175,7 @@ public class JsonSchemaBuilder {
JsonNode enumValues = propertyNode.get(PropertyConstant.ENUM_VALUES); JsonNode enumValues = propertyNode.get(PropertyConstant.ENUM_VALUES);
JsonNode defaultValue = propertyNode.get(PropertyConstant.DEFAULT_VALUE); JsonNode defaultValue = propertyNode.get(PropertyConstant.DEFAULT_VALUE);
if (enumValues != null && enumValues instanceof ArrayNode) { if (enumValues != null && enumValues instanceof ArrayNode) {
value = enumValues.get(new Random().nextInt(enumValues.size())).asText(); value = enumValues.get(new SecureRandom().nextInt(enumValues.size())).asText();
} else if (isTextNotBlank(defaultValue)) { } else if (isTextNotBlank(defaultValue)) {
value = defaultValue.asText(); value = defaultValue.asText();
} else { } else {
@ -178,7 +184,7 @@ public class JsonSchemaBuilder {
int max = isTextNotBlank(maximum) ? maximum.asInt() : Integer.MAX_VALUE; int max = isTextNotBlank(maximum) ? maximum.asInt() : Integer.MAX_VALUE;
int min = isTextNotBlank(minimum) ? minimum.asInt() : Integer.MIN_VALUE; int min = isTextNotBlank(minimum) ? minimum.asInt() : Integer.MIN_VALUE;
// 这里减去负数可能超过整型最大值使用 Long 类型 // 这里减去负数可能超过整型最大值使用 Long 类型
value = new Random().nextLong(Long.valueOf(max) - Long.valueOf(min)) + min + StringUtils.EMPTY; value = new SecureRandom().nextLong(Long.valueOf(max) - Long.valueOf(min)) + min + StringUtils.EMPTY;
} }
} else { } else {
if (isVariable(value)) { if (isVariable(value)) {
@ -196,7 +202,7 @@ public class JsonSchemaBuilder {
JsonNode enumValues = propertyNode.get(PropertyConstant.ENUM_VALUES); JsonNode enumValues = propertyNode.get(PropertyConstant.ENUM_VALUES);
JsonNode defaultValue = propertyNode.get(PropertyConstant.DEFAULT_VALUE); JsonNode defaultValue = propertyNode.get(PropertyConstant.DEFAULT_VALUE);
if (enumValues != null && enumValues instanceof ArrayNode) { if (enumValues != null && enumValues instanceof ArrayNode) {
value = enumValues.get(new Random().nextInt(enumValues.size())).asText(); value = enumValues.get(new SecureRandom().nextInt(enumValues.size())).asText();
} else if (isTextNotBlank(defaultValue)) { } else if (isTextNotBlank(defaultValue)) {
value = defaultValue.asText(); value = defaultValue.asText();
} else { } else {

View File

@ -0,0 +1,16 @@
package io.metersphere.api.utils.regex.exception;
public class RegexpIllegalException extends Exception {
public RegexpIllegalException() {
super();
}
public RegexpIllegalException(String message) {
super(message);
}
public RegexpIllegalException(String regexp, int index) {
super(String.format("Invalid regular expression: %s, Index: %d", regexp, index));
}
}

View File

@ -0,0 +1,12 @@
package io.metersphere.api.utils.regex.exception;
public class TypeNotMatchException extends Exception {
public TypeNotMatchException() {
super();
}
public TypeNotMatchException(String message) {
super(message);
}
}

View File

@ -0,0 +1,12 @@
package io.metersphere.api.utils.regex.exception;
public class UninitializedException extends Exception {
public UninitializedException() {
super();
}
public UninitializedException(String message) {
super(message);
}
}

View File

@ -0,0 +1,171 @@
package io.metersphere.api.utils.regex.model;
import io.metersphere.api.utils.regex.exception.*;
import java.util.ArrayList;
import java.util.List;
/**
* 参考 https://github.com/GitHub-Laziji/reverse-regexp 代码
*
*/
public abstract class BaseNode implements Node {
private String expression;
private List<String> expressionFragments;
private boolean initialized;
protected BaseNode(String expression) throws RegexpIllegalException, TypeNotMatchException {
this(expression, true);
}
protected BaseNode(String expression, boolean initialize) throws RegexpIllegalException, TypeNotMatchException {
this.expression = expression;
this.expressionFragments = spliceExpression(expression);
if (initialize) {
init();
}
}
protected BaseNode(List<String> expressionFragments) throws RegexpIllegalException, TypeNotMatchException {
this(expressionFragments, true);
}
protected BaseNode(List<String> expressionFragments, boolean initialize)
throws RegexpIllegalException, TypeNotMatchException {
this.expressionFragments = expressionFragments;
StringBuilder stringBuilder = new StringBuilder();
for (String fragment : expressionFragments) {
stringBuilder.append(fragment);
}
this.expression = stringBuilder.toString();
if (initialize) {
init();
}
}
@Override
public String random() throws UninitializedException, RegexpIllegalException {
if (!initialized) {
throw new UninitializedException();
}
return random(expression, expressionFragments);
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public boolean test() {
return test(expression, expressionFragments);
}
@Override
public void init() throws RegexpIllegalException, TypeNotMatchException {
if (!initialized) {
if (!test()) {
throw new TypeNotMatchException();
}
init(expression, expressionFragments);
initialized = true;
}
}
@Override
public String getExpression() {
return expression;
}
protected String random(String expression, List<String> expressionFragments)
throws RegexpIllegalException, UninitializedException {
return null;
}
protected void init(String expression, List<String> expressionFragments)
throws RegexpIllegalException, TypeNotMatchException {
}
protected boolean test(String expression, List<String> expressionFragments) {
return true;
}
private List<String> spliceExpression(String expression) throws RegexpIllegalException {
int l = 0;
int r = expression.length();
List<String> fragments = new ArrayList<>();
while (true) {
String result = findFirst(expression, l, r);
if (result == null || result.isEmpty()) {
break;
}
fragments.add(result);
l += result.length();
}
return fragments;
}
private String findFirst(String expression, int l, int r) throws RegexpIllegalException {
if (l == r) {
return null;
}
if (expression.charAt(l) == '\\') {
if (l + 1 >= r) {
throw new RegexpIllegalException(expression, l + 1);
}
return expression.substring(l, l + 2);
}
if (expression.charAt(l) == '[') {
int i = l + 1;
while (i < r) {
if (expression.charAt(i) == ']') {
return expression.substring(l, i + 1);
}
if (expression.charAt(i) == '\\') {
i++;
}
i++;
}
throw new RegexpIllegalException(expression, r);
}
if (expression.charAt(l) == '{') {
int i = l + 1;
boolean hasDelimiter = false;
while (i < r) {
if (expression.charAt(i) == '}') {
return expression.substring(l, i + 1);
}
if (expression.charAt(i) == ',') {
if (hasDelimiter) {
throw new RegexpIllegalException(expression, i);
}
hasDelimiter = true;
i++;
continue;
}
if (expression.charAt(i) < '0' || expression.charAt(i) > '9') {
throw new RegexpIllegalException(expression, i);
}
i++;
}
throw new RegexpIllegalException(expression, r);
}
if (expression.charAt(l) == '(') {
int i = l + 1;
while (true) {
String result = findFirst(expression, i, r);
if (result == null || result.length() == 0 || result.length() + i >= r) {
throw new RegexpIllegalException(expression, i);
}
i += result.length();
if (expression.charAt(i) == ')') {
return expression.substring(l, i + 1);
}
}
}
return expression.substring(l, l + 1);
}
}

View File

@ -0,0 +1,64 @@
package io.metersphere.api.utils.regex.model;
import io.metersphere.api.utils.regex.exception.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class LinkNode extends BaseNode {
private List<Node> children;
protected LinkNode(List<String> expressionFragments) throws RegexpIllegalException, TypeNotMatchException {
super(expressionFragments);
}
protected LinkNode(List<String> expressionFragments, boolean initialize)
throws RegexpIllegalException, TypeNotMatchException {
super(expressionFragments, initialize);
}
@Override
protected boolean test(String expression, List<String> expressionFragments) {
for (String fragment : expressionFragments) {
if ("|".equals(fragment)) {
return false;
}
}
return true;
}
@Override
protected void init(String expression, List<String> expressionFragments)
throws RegexpIllegalException, TypeNotMatchException {
children = new ArrayList<>();
for (int i = 0; i < expressionFragments.size(); i++) {
Node node;
if (i + 1 < expressionFragments.size()) {
node = new RepeatNode(
Arrays.asList(expressionFragments.get(i), expressionFragments.get(i + 1)),
false);
if (node.test()) {
node.init();
children.add(node);
i++;
continue;
}
}
node = new SingleNode(Collections.singletonList(expressionFragments.get(i)));
children.add(node);
}
}
@Override
protected String random(String expression, List<String> expressionFragments)
throws RegexpIllegalException, UninitializedException {
StringBuilder value = new StringBuilder();
for (Node node : children) {
value.append(node.random());
}
return value.toString();
}
}

View File

@ -0,0 +1,16 @@
package io.metersphere.api.utils.regex.model;
import io.metersphere.api.utils.regex.exception.*;
public interface Node {
String getExpression();
String random() throws UninitializedException, RegexpIllegalException;
boolean test();
void init() throws RegexpIllegalException, TypeNotMatchException;
boolean isInitialized();
}

View File

@ -0,0 +1,54 @@
package io.metersphere.api.utils.regex.model;
import io.metersphere.api.utils.regex.exception.*;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class OptionalNode extends BaseNode {
private List<Node> children;
protected OptionalNode(List<String> expressionFragments) throws RegexpIllegalException, TypeNotMatchException {
super(expressionFragments);
}
protected OptionalNode(List<String> expressionFragments, boolean initialize)
throws RegexpIllegalException, TypeNotMatchException {
super(expressionFragments, initialize);
}
@Override
protected boolean test(String expression, List<String> expressionFragments) {
for (String fragment : expressionFragments) {
if ("|".equals(fragment)) {
return true;
}
}
return false;
}
@Override
protected void init(String expression, List<String> expressionFragments)
throws RegexpIllegalException, TypeNotMatchException {
children = new ArrayList<>();
List<String> subFragments = new ArrayList<>();
for (String fragment : expressionFragments) {
if ("|".equals(fragment)) {
children.add(new OrdinaryNode(subFragments));
subFragments = new ArrayList<>();
continue;
}
subFragments.add(fragment);
}
children.add(new OrdinaryNode(subFragments));
}
@Override
protected String random(String expression, List<String> expressionFragments)
throws UninitializedException, RegexpIllegalException {
return children.get(new SecureRandom().nextInt(children.size())).random();
}
}

View File

@ -0,0 +1,47 @@
package io.metersphere.api.utils.regex.model;
import io.metersphere.api.utils.regex.exception.*;
import java.util.List;
public class OrdinaryNode extends BaseNode {
private Node proxyNode;
public OrdinaryNode(String expression) throws RegexpIllegalException, TypeNotMatchException {
super(expression);
}
protected OrdinaryNode(List<String> expressionFragments) throws RegexpIllegalException, TypeNotMatchException {
super(expressionFragments);
}
@Override
protected void init(String expression, List<String> expressionFragments)
throws RegexpIllegalException, TypeNotMatchException {
if (expressionFragments.size() == 0) {
return;
}
Node[] nodes = new Node[]{
new OptionalNode(expressionFragments, false),
new SingleNode(expressionFragments, false),
new RepeatNode(expressionFragments, false),
new LinkNode(expressionFragments, false)
};
for (Node node : nodes) {
if (node.test()) {
proxyNode = node;
proxyNode.init();
break;
}
}
}
@Override
protected String random(String expression, List<String> expressionFragments)
throws UninitializedException, RegexpIllegalException {
if (proxyNode == null) {
return "";
}
return proxyNode.random();
}
}

View File

@ -0,0 +1,72 @@
package io.metersphere.api.utils.regex.model;
import io.metersphere.api.utils.regex.exception.*;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.List;
import java.util.Random;
public class RepeatNode extends BaseNode {
private static final int MAX_REPEAT = 16;
private Node node;
private int minRepeat = 1;
private int maxRepeat = 1;
protected RepeatNode(List<String> expressionFragments) throws RegexpIllegalException, TypeNotMatchException {
super(expressionFragments);
}
protected RepeatNode(List<String> expressionFragments, boolean initialize)
throws RegexpIllegalException, TypeNotMatchException {
super(expressionFragments, initialize);
}
@Override
protected boolean test(String expression, List<String> expressionFragments) {
if (expressionFragments.size() == 2) {
String token = expressionFragments.get(1);
return token != null
&& ("+".equals(token) || "?".equals(token) || "*".equals(token) || token.startsWith("{"));
}
return false;
}
@Override
protected void init(String expression, List<String> expressionFragments)
throws RegexpIllegalException, TypeNotMatchException {
node = new SingleNode(Collections.singletonList(expressionFragments.get(0)));
String token = expressionFragments.get(1);
if ("+".equals(token)) {
maxRepeat = MAX_REPEAT;
} else if ("?".equals(token)) {
minRepeat = 0;
} else if ("*".equals(token)) {
minRepeat = 0;
maxRepeat = MAX_REPEAT;
} else if (token.startsWith("{")) {
String[] numbers = token.substring(1, token.length() - 1).split(",", 2);
minRepeat = maxRepeat = Integer.parseInt(numbers[0]);
if (numbers.length > 1) {
maxRepeat = numbers[1].isEmpty() ? Math.max(MAX_REPEAT, minRepeat) : Integer.parseInt(numbers[1]);
if (maxRepeat < minRepeat) {
throw new RegexpIllegalException("Invalid regular expression: "
+ getExpression() + " : Numbers out of order in {} quantifier");
}
}
}
}
@Override
protected String random(String expression, List<String> expressionFragments)
throws RegexpIllegalException, UninitializedException {
int repeat = new SecureRandom().nextInt(maxRepeat - minRepeat + 1) + minRepeat;
StringBuilder value = new StringBuilder();
while (repeat-- > 0) {
value.append(node.random());
}
return value.toString();
}
}

View File

@ -0,0 +1,152 @@
package io.metersphere.api.utils.regex.model;
import io.metersphere.api.utils.regex.exception.*;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class SingleNode extends BaseNode {
private Node node;
private List<Interval> intervals;
protected SingleNode(List<String> expressionFragments) throws RegexpIllegalException, TypeNotMatchException {
super(expressionFragments);
}
protected SingleNode(List<String> expressionFragments, boolean initialize)
throws RegexpIllegalException, TypeNotMatchException {
super(expressionFragments, initialize);
}
@Override
protected boolean test(String expression, List<String> expressionFragments) {
return expressionFragments != null && expressionFragments.size() == 1;
}
@Override
protected void init(String expression, List<String> expressionFragments)
throws RegexpIllegalException, TypeNotMatchException {
if (expression.startsWith("(")) {
node = new OrdinaryNode(expression.substring(1, expression.length() - 1));
return;
}
if (expression.startsWith("[")) {
int i = 1;
Character preChar = null;
while (i < expression.length() - 1) {
if (expression.charAt(i) == '\\') {
if (i + 1 >= expression.length() - 1) {
throw new RegexpIllegalException(expression, i);
}
if (preChar != null && "dws".contains(expression.charAt(i + 1) + "")) {
addIntervals(preChar, null, '-', null);
preChar = null;
}
if (expression.charAt(i + 1) == 'd') {
addIntervals('0', '9');
} else if (expression.charAt(i + 1) == 'w') {
addIntervals('0', '9', 'A', 'Z', 'a', 'z', '_', null);
} else if (expression.charAt(i + 1) == 's') {
addIntervals(' ', null, '\t', null);
} else {
if (preChar != null) {
addIntervals(preChar, expression.charAt(i + 1));
preChar = null;
} else if (i + 2 < expression.length() && expression.charAt(i + 2) == '-') {
preChar = expression.charAt(i + 1);
i++;
} else {
addIntervals(expression.charAt(i + 1), null);
}
}
i++;
} else if (preChar != null) {
addIntervals(preChar, expression.charAt(i));
preChar = null;
} else if (i + 1 < expression.length() && expression.charAt(i + 1) == '-') {
preChar = expression.charAt(i);
i++;
} else {
addIntervals(expression.charAt(i), null);
}
i++;
}
if (preChar != null) {
addIntervals(preChar, null, '-', null);
}
} else if (".".equals(expression)) {
// 这里仅包含一般字符参考 ASCII 码表
addIntervals((char) 33, (char) 126);
} else if ("\\s".equals(expression)) {
addIntervals(' ', null, '\t', null);
} else if ("\\d".equals(expression)) {
addIntervals('0', '9');
} else if ("\\w".equals(expression)) {
addIntervals('0', '9', 'A', 'Z', 'a', 'z', '_', null);
} else if (expression.startsWith("\\")) {
addIntervals(expression.charAt(1), null);
}
}
@Override
protected String random(String expression, List<String> expressionFragments)
throws RegexpIllegalException, UninitializedException {
if (node != null) {
return node.random();
}
if (intervals != null && intervals.size() > 0) {
Character value = randomCharFromInterval(intervals.toArray(new Interval[0]));
return value == null ? "" : value.toString();
}
return expression;
}
private Character randomCharFromInterval(Interval... intervals) {
int count = 0;
for (Interval interval : intervals) {
count += interval.end + 1 - interval.start;
}
int randomValue = new SecureRandom().nextInt(count);
for (Interval interval : intervals) {
if (randomValue < interval.end + 1 - interval.start) {
return (char) (interval.start + randomValue);
}
randomValue -= interval.end + 1 - interval.start;
}
return null;
}
private void addIntervals(Character... chars) throws RegexpIllegalException {
if (intervals == null) {
intervals = new ArrayList<>();
}
for (int i = 0; i + 1 < chars.length; i += 2) {
Character start = chars[i];
Character end = chars[i + 1] == null ? start : chars[i + 1];
if (start == null) {
throw new RegexpIllegalException("Invalid regular expression: "
+ getExpression() + " : Character class is null");
}
if (end < start) {
throw new RegexpIllegalException("Invalid regular expression: "
+ getExpression() + " : Range out of order in character class");
}
intervals.add(new Interval(start, end));
}
}
private static class Interval {
private char start;
private char end;
private Interval(char start, char end) {
this.start = start;
this.end = end;
}
}
}

View File

@ -85,7 +85,6 @@
<commons-jexl3.version>3.3</commons-jexl3.version> <commons-jexl3.version>3.3</commons-jexl3.version>
<revision>3.x</revision> <revision>3.x</revision>
<monitoring-engine.revision>3.0</monitoring-engine.revision> <monitoring-engine.revision>3.0</monitoring-engine.revision>
<xeger.version>1.0.0-RELEASE</xeger.version>
</properties> </properties>
<modules> <modules>