Skip to content

Commit ef024a6

Browse files
committed
fix: Simple body playbooks must run once per path+http method
When having oneOf/anyOf payloads that can result in multiple requests for the same path and http method, run simple body playbooks once
1 parent b6ae652 commit ef024a6

22 files changed

Lines changed: 264 additions & 354 deletions

src/main/java/dev/dochia/cli/core/factory/PlaybookDataFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -734,7 +734,7 @@ private Set<DochiaHeader> extractHeaders(Operation operation) {
734734
headers.add(DochiaHeader.fromHeaderParameter(param));
735735
} catch (IllegalArgumentException _) {
736736
globalContext.recordError("A valid string could not be generated for the header '" + param.getName() + "' using the pattern '" + param.getSchema().getPattern() + "'. Please consider either changing the pattern or simplifying it.");
737-
headers.add(DochiaHeader.from(param.getName(), OpenAPIModelGenerator.DEFAULT_STRING_WHEN_GENERATION_FAILS, param.getRequired()));
737+
headers.add(DochiaHeader.from(param.getName(), OpenAPIModelGenerator.DEFAULT_STRING_WHEN_GENERATION_FAILS, param.getRequired(), Optional.ofNullable(param.getSchema()).map(Schema::getFormat).orElse(null)));
738738
}
739739
}
740740
}

src/main/java/dev/dochia/cli/core/model/DochiaHeader.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,21 @@ public class DochiaHeader {
2323
private final boolean required;
2424
private final String name;
2525
private String value;
26+
private String format;
27+
2628

2729
private DochiaHeader(Parameter param) {
2830
this.name = param.getName();
2931
this.required = param.getRequired() != null && param.getRequired();
3032
this.value = this.generateValue(param.getSchema());
33+
this.format = param.getSchema().getFormat();
3134
}
3235

33-
private DochiaHeader(String name, String value, Boolean required) {
36+
private DochiaHeader(String name, String value, Boolean required, String format) {
3437
this.name = name;
3538
this.required = required != null && required;
3639
this.value = value;
40+
this.format = format;
3741
}
3842

3943
/**
@@ -52,10 +56,11 @@ public static DochiaHeader fromHeaderParameter(Parameter param) {
5256
* @param name the name of the header
5357
* @param value the value of the header
5458
* @param required whether the header is required or not
59+
* @param format the format of the header
5560
* @return a new DochiaHeader object
5661
*/
57-
public static DochiaHeader from(String name, String value, Boolean required) {
58-
return new DochiaHeader(name, value, required);
62+
public static DochiaHeader from(String name, String value, Boolean required, String format) {
63+
return new DochiaHeader(name, value, required, format);
5964
}
6065

6166
/**
@@ -87,7 +92,7 @@ public String getTruncatedValue() {
8792
* @return a copy of the current header
8893
*/
8994
public DochiaHeader copy() {
90-
return DochiaHeader.builder().name(this.name).required(this.required).value(this.value).build();
95+
return DochiaHeader.builder().name(this.name).required(this.required).value(this.value).format(this.format).build();
9196
}
9297

9398
private String generateValue(Schema schema) {

src/main/java/dev/dochia/cli/core/playbook/body/BaseHttpWithPayloadSimplePlaybook.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import io.github.ludovicianul.prettylogger.PrettyLogger;
1313
import io.github.ludovicianul.prettylogger.PrettyLoggerFactory;
1414

15+
import java.util.ArrayList;
1516
import java.util.Arrays;
1617
import java.util.List;
1718

@@ -22,6 +23,7 @@
2223
public abstract class BaseHttpWithPayloadSimplePlaybook implements TestCasePlaybook {
2324
private final PrettyLogger logger = PrettyLoggerFactory.getLogger(getClass());
2425
private final SimpleExecutor simpleExecutor;
26+
private final List<String> fuzzedPaths = new ArrayList<>();
2527

2628
BaseHttpWithPayloadSimplePlaybook(SimpleExecutor ce) {
2729
this.simpleExecutor = ce;
@@ -34,6 +36,13 @@ public void run(PlaybookData data) {
3436
return;
3537
}
3638

39+
String runKey = data.getPath() + data.getMethod();
40+
if (fuzzedPaths.contains(runKey)) {
41+
logger.skip("Skipping playbook as it already tested. Most likely a oneOf/anyOf payload");
42+
return;
43+
}
44+
fuzzedPaths.add(runKey);
45+
3746
simpleExecutor.execute(
3847
SimpleExecutorContext.builder()
3948
.expectedResponseCode(this.getExpectedResponseCode(data))

src/main/java/dev/dochia/cli/core/playbook/executor/HeadersIteratorExecutor.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import dev.dochia.cli.core.strategy.FuzzingStrategy;
1414
import jakarta.inject.Singleton;
1515

16+
import java.util.Optional;
1617
import java.util.Set;
1718
import java.util.stream.Collectors;
1819

@@ -63,8 +64,7 @@ public void execute(HeadersIteratorExecutorContext context) {
6364
header.withValue(String.valueOf(fuzzingStrategy.process(previousHeaderValue)));
6465
try {
6566
testCaseListener.createAndExecuteTest(context.getLogger(), context.getTestCasePlaybook(), () -> {
66-
boolean isRequiredHeaderFuzzed = clonedHeaders.stream().filter(DochiaHeader::isRequired).toList().contains(header);
67-
ResponseCodeFamily expectedResponseCode = this.getExpectedResultCode(isRequiredHeaderFuzzed, context);
67+
ResponseCodeFamily expectedResponseCode = this.getExpectedResultCode(header, context);
6868

6969
testCaseListener.addScenario(context.getLogger(), context.getScenario() + " Current header [{}] [{}]", header.getName(), fuzzingStrategy);
7070
testCaseListener.addExpectedResult(context.getLogger(), "Should return [{}]",
@@ -109,8 +109,9 @@ private void reportResult(HeadersIteratorExecutorContext context, ResponseCodeFa
109109
}
110110
}
111111

112-
ResponseCodeFamily getExpectedResultCode(boolean required, HeadersIteratorExecutorContext context) {
113-
return required ? context.getExpectedResponseCodeForRequiredHeaders() : context.getExpectedResponseCodeForOptionalHeaders();
112+
ResponseCodeFamily getExpectedResultCode(DochiaHeader dochiaHeader, HeadersIteratorExecutorContext context) {
113+
return dochiaHeader.isRequired() ? context.getExpectedResponseCodeForRequiredHeaders() :
114+
Optional.ofNullable(context.getExpectedResponseCodeForOptionalHeadersProducer()).orElse(_ -> null).apply(dochiaHeader);
114115
}
115116

116117
private Set<DochiaHeader> getHeadersWithoutAuthHeaders(HeadersIteratorExecutorContext context) {

src/main/java/dev/dochia/cli/core/playbook/executor/HeadersIteratorExecutorContext.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package dev.dochia.cli.core.playbook.executor;
22

3+
import dev.dochia.cli.core.model.DochiaHeader;
34
import dev.dochia.cli.core.playbook.api.TestCasePlaybook;
45
import dev.dochia.cli.core.http.ResponseCodeFamily;
56
import dev.dochia.cli.core.model.PlaybookData;
@@ -9,6 +10,7 @@
910
import lombok.Value;
1011

1112
import java.util.List;
13+
import java.util.function.Function;
1214
import java.util.function.Supplier;
1315

1416
/**
@@ -31,7 +33,7 @@ public class HeadersIteratorExecutorContext {
3133
* If you provide an expected response code, it will be used and matched against what the service returns and report info, warn or error accordingly.
3234
* If not supplied, FieldsIteratorExecutor will test for Marching Arguments. If any match is found it will be reported as error, otherwise the test will be marked as skipped.
3335
*/
34-
ResponseCodeFamily expectedResponseCodeForOptionalHeaders;
36+
Function<DochiaHeader, ResponseCodeFamily> expectedResponseCodeForOptionalHeadersProducer;
3537
TestCasePlaybook testCasePlaybook;
3638

3739
Supplier<List<FuzzingStrategy>> fuzzValueProducer;

src/main/java/dev/dochia/cli/core/playbook/field/DuplicateKeysFieldsPlaybook.java

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@
55
import com.google.gson.JsonParser;
66
import dev.dochia.cli.core.http.HttpMethod;
77
import dev.dochia.cli.core.http.ResponseCodeFamilyPredefined;
8-
import dev.dochia.cli.core.io.ServiceCaller;
98
import dev.dochia.cli.core.io.ServiceData;
10-
import dev.dochia.cli.core.model.HttpResponse;
119
import dev.dochia.cli.core.model.PlaybookData;
1210
import dev.dochia.cli.core.playbook.api.FieldPlaybook;
1311
import dev.dochia.cli.core.playbook.api.TestCasePlaybook;
14-
import dev.dochia.cli.core.report.TestCaseListener;
12+
import dev.dochia.cli.core.playbook.executor.SimpleExecutor;
13+
import dev.dochia.cli.core.playbook.executor.SimpleExecutorContext;
1514
import dev.dochia.cli.core.util.ConsoleUtils;
1615
import dev.dochia.cli.core.util.JsonUtils;
1716
import io.github.ludovicianul.prettylogger.PrettyLogger;
@@ -39,13 +38,10 @@ public class DuplicateKeysFieldsPlaybook implements TestCasePlaybook {
3938
private static final String DUPLICATE_VALUE = "dochiaFuzzyDup";
4039
private static final int MAX_FIELD_DEPTH = 5;
4140
private static final int MAX_MUTATIONS_PER_REQUEST = 100;
41+
private final SimpleExecutor simpleExecutor;
4242

43-
private final ServiceCaller serviceCaller;
44-
private final TestCaseListener testCaseListener;
45-
46-
public DuplicateKeysFieldsPlaybook(ServiceCaller serviceCaller, TestCaseListener testCaseListener) {
47-
this.serviceCaller = serviceCaller;
48-
this.testCaseListener = testCaseListener;
43+
public DuplicateKeysFieldsPlaybook(SimpleExecutor simpleExecutor) {
44+
this.simpleExecutor = simpleExecutor;
4945
}
5046

5147
@Override
@@ -92,12 +88,7 @@ private void executeFieldDuplication(PlaybookData data, String field) {
9288
return;
9389
}
9490

95-
testCaseListener.createAndExecuteTest(
96-
logger,
97-
this,
98-
() -> runDuplicationTestCase(data, field, duplicatedPayload.get()),
99-
data
100-
);
91+
runDuplicationTestCase(data, field, duplicatedPayload.get());
10192
}
10293

10394
private Optional<String> createDuplicatedPayload(String originalPayload, String fieldPath) {
@@ -116,18 +107,16 @@ private Optional<String> createDuplicatedPayload(String originalPayload, String
116107
}
117108

118109
private void runDuplicationTestCase(PlaybookData data, String field, String duplicatedPayload) {
119-
testCaseListener.addScenario(logger,
120-
"Duplicate key [{}] in parent object with second value [{}]", field, DUPLICATE_VALUE);
121-
testCaseListener.addExpectedResult(logger,
122-
"Service should return a [{}] response", ResponseCodeFamilyPredefined.FOURXX.asString());
123-
124-
if (!testCaseListener.shouldContinueExecution(logger, ResponseCodeFamilyPredefined.FOURXX)) {
125-
testCaseListener.skipTest(logger, "Test skipped due to response code filtering");
126-
return;
127-
}
128-
129-
HttpResponse response = serviceCaller.call(buildServiceData(data, duplicatedPayload));
130-
testCaseListener.reportResult(logger, data, response, ResponseCodeFamilyPredefined.FOURXX);
110+
simpleExecutor.execute(SimpleExecutorContext.builder()
111+
.logger(logger)
112+
.testCasePlaybook(this)
113+
.playbookData(data)
114+
.payload(duplicatedPayload)
115+
.expectedResponseCode(ResponseCodeFamilyPredefined.FOURXX)
116+
.scenario("Duplicate key [" + field + "] in parent object with second value [" + DUPLICATE_VALUE + "]")
117+
.replaceRefData(false)
118+
.validJson(false)
119+
.build());
131120
}
132121

133122
private ServiceData buildServiceData(PlaybookData data, String payload) {

src/main/java/dev/dochia/cli/core/playbook/field/InsertWhitespacesInFieldNamesFieldPlaybook.java

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
package dev.dochia.cli.core.playbook.field;
22

3-
import dev.dochia.cli.core.playbook.api.FieldPlaybook;
4-
import dev.dochia.cli.core.playbook.api.TestCasePlaybook;
53
import dev.dochia.cli.core.generator.simple.UnicodeGenerator;
64
import dev.dochia.cli.core.http.HttpMethod;
75
import dev.dochia.cli.core.http.ResponseCodeFamilyPredefined;
8-
import dev.dochia.cli.core.io.ServiceCaller;
9-
import dev.dochia.cli.core.io.ServiceData;
10-
import dev.dochia.cli.core.model.HttpResponse;
116
import dev.dochia.cli.core.model.PlaybookData;
12-
import dev.dochia.cli.core.report.TestCaseListener;
7+
import dev.dochia.cli.core.playbook.api.FieldPlaybook;
8+
import dev.dochia.cli.core.playbook.api.TestCasePlaybook;
9+
import dev.dochia.cli.core.playbook.executor.SimpleExecutor;
10+
import dev.dochia.cli.core.playbook.executor.SimpleExecutorContext;
1311
import dev.dochia.cli.core.util.ConsoleUtils;
1412
import dev.dochia.cli.core.util.JsonUtils;
1513
import io.github.ludovicianul.prettylogger.PrettyLogger;
@@ -23,18 +21,15 @@
2321
@Singleton
2422
public class InsertWhitespacesInFieldNamesFieldPlaybook implements TestCasePlaybook {
2523
private final PrettyLogger logger = PrettyLoggerFactory.getLogger(InsertWhitespacesInFieldNamesFieldPlaybook.class);
26-
private final ServiceCaller serviceCaller;
27-
private final TestCaseListener testCaseListener;
24+
private final SimpleExecutor simpleExecutor;
2825

2926
/**
3027
* Creates a new InsertWhitespacesInFieldNamesFieldPlaybook instance.
3128
*
32-
* @param sc the service caller
33-
* @param lr the test case listener
29+
* @param simpleExecutor the simple executor
3430
*/
35-
public InsertWhitespacesInFieldNamesFieldPlaybook(ServiceCaller sc, TestCaseListener lr) {
36-
this.serviceCaller = sc;
37-
this.testCaseListener = lr;
31+
public InsertWhitespacesInFieldNamesFieldPlaybook(SimpleExecutor simpleExecutor) {
32+
this.simpleExecutor = simpleExecutor;
3833
}
3934

4035
@Override
@@ -50,27 +45,22 @@ public void run(PlaybookData data) {
5045
for (String field : allFieldsByHttpMethod) {
5146
logger.debug("Fuzzing field {}, inserting {}", field, randomWhitespace);
5247
if (JsonUtils.isFieldInJson(data.getPayload(), field)) {
53-
testCaseListener.createAndExecuteTest(logger, this, () -> process(data, field, randomWhitespace), data);
48+
process(data, field, randomWhitespace);
5449
}
5550
}
5651
}
5752

5853
private void process(PlaybookData data, String field, String randomWhitespace) {
59-
testCaseListener.addScenario(logger, "Insert random whitespaces in the field name [{}]", field);
60-
testCaseListener.addExpectedResult(logger, "Should get a [{}] response code", ResponseCodeFamilyPredefined.FOURXX);
61-
62-
if (!testCaseListener.shouldContinueExecution(logger, ResponseCodeFamilyPredefined.FOURXX)) {
63-
testCaseListener.skipTest(logger, "Test skipped due to response code filtering");
64-
return;
65-
}
66-
6754
String fuzzedJson = JsonUtils.insertCharactersInFieldKey(data.getPayload(), field, randomWhitespace);
6855

69-
HttpResponse response = serviceCaller.call(ServiceData.builder().relativePath(data.getPath()).headers(data.getHeaders())
70-
.payload(fuzzedJson).queryParams(data.getQueryParams()).httpMethod(data.getMethod()).contractPath(data.getContractPath())
71-
.contentType(data.getFirstRequestContentType()).pathParamsPayload(data.getPathParamsPayload())
56+
simpleExecutor.execute(SimpleExecutorContext.builder()
57+
.logger(logger)
58+
.testCasePlaybook(this)
59+
.playbookData(data)
60+
.payload(fuzzedJson)
61+
.expectedResponseCode(ResponseCodeFamilyPredefined.FOURXX)
62+
.scenario("Insert random whitespaces in the field name [" + field + "]")
7263
.build());
73-
testCaseListener.reportResult(logger, data, response, ResponseCodeFamilyPredefined.FOURXX);
7464
}
7565

7666
@Override

src/main/java/dev/dochia/cli/core/playbook/field/NewFieldsPlaybook.java

Lines changed: 20 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
package dev.dochia.cli.core.playbook.field;
22

3-
import dev.dochia.cli.core.playbook.api.FieldPlaybook;
4-
import dev.dochia.cli.core.playbook.api.TestCasePlaybook;
3+
import com.google.gson.JsonArray;
4+
import com.google.gson.JsonElement;
5+
import com.google.gson.JsonObject;
6+
import com.google.gson.JsonParser;
57
import dev.dochia.cli.core.http.HttpMethod;
68
import dev.dochia.cli.core.http.ResponseCodeFamily;
79
import dev.dochia.cli.core.http.ResponseCodeFamilyPredefined;
8-
import dev.dochia.cli.core.io.ServiceCaller;
9-
import dev.dochia.cli.core.io.ServiceData;
10-
import dev.dochia.cli.core.model.HttpResponse;
1110
import dev.dochia.cli.core.model.PlaybookData;
12-
import dev.dochia.cli.core.report.TestCaseListener;
11+
import dev.dochia.cli.core.playbook.api.FieldPlaybook;
12+
import dev.dochia.cli.core.playbook.api.TestCasePlaybook;
13+
import dev.dochia.cli.core.playbook.executor.SimpleExecutor;
14+
import dev.dochia.cli.core.playbook.executor.SimpleExecutorContext;
1315
import dev.dochia.cli.core.util.ConsoleUtils;
1416
import dev.dochia.cli.core.util.JsonUtils;
15-
import com.google.gson.JsonArray;
16-
import com.google.gson.JsonElement;
17-
import com.google.gson.JsonObject;
18-
import com.google.gson.JsonParser;
1917
import io.github.ludovicianul.prettylogger.PrettyLogger;
2018
import io.github.ludovicianul.prettylogger.PrettyLoggerFactory;
2119
import jakarta.inject.Singleton;
@@ -29,49 +27,38 @@
2927
@FieldPlaybook
3028
public class NewFieldsPlaybook implements TestCasePlaybook {
3129
private final PrettyLogger logger = PrettyLoggerFactory.getLogger(NewFieldsPlaybook.class);
32-
private final ServiceCaller serviceCaller;
33-
private final TestCaseListener testCaseListener;
30+
private final SimpleExecutor simpleExecutor;
3431

3532
/**
3633
* Creates a new NewFieldsPlaybook instance.
3734
*
38-
* @param sc the service caller
39-
* @param lr the test case listener
35+
* @param simpleExecutor the simple executor
4036
*/
41-
public NewFieldsPlaybook(ServiceCaller sc, TestCaseListener lr) {
42-
this.serviceCaller = sc;
43-
this.testCaseListener = lr;
37+
public NewFieldsPlaybook(SimpleExecutor simpleExecutor) {
38+
this.simpleExecutor = simpleExecutor;
4439
}
4540

4641
@Override
4742
public void run(PlaybookData data) {
48-
testCaseListener.createAndExecuteTest(logger, this, () -> process(data), data);
49-
}
50-
51-
private void process(PlaybookData data) {
5243
String fuzzedJson = this.addNewField(data);
5344
if (JsonUtils.equalAsJson(fuzzedJson, data.getPayload())) {
54-
testCaseListener.skipTest(logger, "Could not fuzz the payload");
45+
logger.skip("Could not add new field to the payload. Skipping fuzzing");
5546
return;
5647
}
5748

5849
ResponseCodeFamily expectedResultCode = ResponseCodeFamilyPredefined.TWOXX;
5950
if (HttpMethod.requiresBody(data.getMethod())) {
6051
expectedResultCode = ResponseCodeFamilyPredefined.FOURXX;
6152
}
62-
testCaseListener.addScenario(logger, "Add new field inside the request: name [{}], value [{}]. All other details are similar to a happy flow", NEW_FIELD, NEW_FIELD);
63-
testCaseListener.addExpectedResult(logger, "Should get a [{}] response code", expectedResultCode.asString());
64-
65-
if (!testCaseListener.shouldContinueExecution(logger, expectedResultCode)) {
66-
testCaseListener.skipTest(logger, "Test skipped due to response code filtering");
67-
return;
68-
}
6953

70-
HttpResponse response = serviceCaller.call(ServiceData.builder().relativePath(data.getPath()).headers(data.getHeaders())
71-
.payload(fuzzedJson).queryParams(data.getQueryParams()).httpMethod(data.getMethod()).contractPath(data.getContractPath())
72-
.contentType(data.getFirstRequestContentType()).pathParamsPayload(data.getPathParamsPayload())
54+
simpleExecutor.execute(SimpleExecutorContext.builder()
55+
.logger(logger)
56+
.testCasePlaybook(this)
57+
.playbookData(data)
58+
.payload(fuzzedJson)
59+
.expectedResponseCode(expectedResultCode)
60+
.scenario("Add new field inside the request: name [" + NEW_FIELD + "], value [" + NEW_FIELD + "]. All other details are similar to a happy flow")
7361
.build());
74-
testCaseListener.reportResult(logger, data, response, expectedResultCode);
7562
}
7663

7764
String addNewField(PlaybookData data) {

0 commit comments

Comments
 (0)