Skip to content

Commit 12d2c5c

Browse files
committed
feat: Add new linter to multiple success reponse codes
1 parent af64563 commit 12d2c5c

File tree

4 files changed

+150
-4
lines changed

4 files changed

+150
-4
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.endava.cats.fuzzer.contract;
2+
3+
import com.endava.cats.annotations.LinterFuzzer;
4+
import com.endava.cats.http.HttpMethod;
5+
import com.endava.cats.model.FuzzingData;
6+
import com.endava.cats.report.TestCaseListener;
7+
import io.github.ludovicianul.prettylogger.PrettyLogger;
8+
import io.github.ludovicianul.prettylogger.PrettyLoggerFactory;
9+
import io.swagger.v3.oas.models.Operation;
10+
import jakarta.inject.Singleton;
11+
12+
import java.util.ArrayList;
13+
import java.util.List;
14+
15+
@LinterFuzzer
16+
@Singleton
17+
public class MultipleSuccessCodesLinterFuzzer extends BaseLinterFuzzer {
18+
private final PrettyLogger log = PrettyLoggerFactory.getLogger(this.getClass());
19+
20+
public MultipleSuccessCodesLinterFuzzer(TestCaseListener tcl) {
21+
super(tcl);
22+
}
23+
24+
@Override
25+
public void process(FuzzingData data) {
26+
testCaseListener.addScenario(log, "Check if an operation defines more than one success (2xx) status code");
27+
testCaseListener.addExpectedResult(log, "Each operation should ideally define a single 2xx response for clarity and predictability");
28+
29+
List<String> violations = new ArrayList<>();
30+
Operation operation = HttpMethod.getOperation(data.getMethod(), data.getPathItem());
31+
32+
if (operation.getResponses() != null) {
33+
List<String> successCodes = operation.getResponses().keySet().stream()
34+
.filter(code -> code.startsWith("2"))
35+
.toList();
36+
37+
if (successCodes.size() > 1) {
38+
violations.add("Operation defines multiple 2xx status codes: %s"
39+
.formatted(successCodes));
40+
}
41+
}
42+
43+
if (!violations.isEmpty()) {
44+
testCaseListener.reportResultWarn(
45+
log,
46+
data,
47+
"Multiple 2xx success status codes found",
48+
String.join("\n", violations)
49+
);
50+
} else {
51+
testCaseListener.reportResultInfo(log, data, "Each operation defines at most one success (2xx) response code");
52+
}
53+
}
54+
55+
@Override
56+
protected String runKey(FuzzingData data) {
57+
return data.getPath() + data.getMethod();
58+
}
59+
60+
@Override
61+
public String description() {
62+
return "flags operations that define more than one 2xx success response, which may cause ambiguity for SDKs and clients";
63+
}
64+
}

src/test/java/com/endava/cats/args/FilterArgumentsTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ void shouldIncludeAllFuzzers() {
7878
List<String> fuzzers = filterArguments.getFirstPhaseFuzzersForPath();
7979

8080
Assertions.assertThat(fuzzers).contains("LeadingControlCharsInHeadersFuzzer", "LeadingWhitespacesInHeadersFuzzer", "LeadingMultiCodePointEmojisInFieldsTrimValidateFuzzer"
81-
, "RemoveFieldsFuzzer", "CheckSecurityHeadersFuzzer").hasSize(158);
81+
, "RemoveFieldsFuzzer", "CheckSecurityHeadersFuzzer").hasSize(161);
8282
}
8383

8484
@Test
@@ -145,7 +145,7 @@ void shouldReturnGetAndDeleteWhenNotHttpMethodSupplied() {
145145

146146
@Test
147147
void shouldReturnAllRegisteredFuzzers() {
148-
Assertions.assertThat(filterArguments.getAllRegisteredFuzzers()).hasSize(163);
148+
Assertions.assertThat(filterArguments.getAllRegisteredFuzzers()).hasSize(166);
149149
}
150150

151151
@Test

src/test/java/com/endava/cats/command/LintCommandTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ void shouldRunContractFuzzers() {
4545
ReflectionTestUtils.setField(lintCommand, "contract", "src/test/resources/petstore-empty.yml");
4646

4747
lintCommand.run();
48-
Assertions.assertThat(filterArguments.getFirstPhaseFuzzersForPath()).hasSize(27);
49-
Mockito.verify(testCaseListener, Mockito.times(27)).afterFuzz("/pets/{id}");
48+
Assertions.assertThat(filterArguments.getFirstPhaseFuzzersForPath()).hasSize(30);
49+
Mockito.verify(testCaseListener, Mockito.times(30)).afterFuzz("/pets/{id}");
5050
}
5151

5252
@Test
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.endava.cats.fuzzer.contract;
2+
3+
import com.endava.cats.args.IgnoreArguments;
4+
import com.endava.cats.args.ReportingArguments;
5+
import com.endava.cats.context.CatsGlobalContext;
6+
import com.endava.cats.http.HttpMethod;
7+
import com.endava.cats.model.FuzzingData;
8+
import com.endava.cats.report.ExecutionStatisticsListener;
9+
import com.endava.cats.report.TestCaseExporter;
10+
import com.endava.cats.report.TestCaseExporterHtmlJs;
11+
import com.endava.cats.report.TestCaseListener;
12+
import io.quarkus.test.junit.QuarkusTest;
13+
import io.swagger.parser.OpenAPIParser;
14+
import io.swagger.v3.oas.models.OpenAPI;
15+
import io.swagger.v3.oas.models.media.Schema;
16+
import jakarta.enterprise.inject.Instance;
17+
import org.assertj.core.api.Assertions;
18+
import org.junit.jupiter.api.BeforeEach;
19+
import org.junit.jupiter.api.Test;
20+
import org.mockito.Mockito;
21+
22+
import java.nio.file.Files;
23+
import java.nio.file.Paths;
24+
import java.util.stream.Stream;
25+
26+
@QuarkusTest
27+
class MultipleSuccessCodesLinterFuzzerTest {
28+
29+
private TestCaseListener testCaseListener;
30+
private MultipleSuccessCodesLinterFuzzer multipleSuccessCodesLinterFuzzer;
31+
32+
@BeforeEach
33+
void setup() {
34+
Instance<TestCaseExporter> exporters = Mockito.mock(Instance.class);
35+
TestCaseExporter exporter = Mockito.mock(TestCaseExporterHtmlJs.class);
36+
Mockito.when(exporters.stream()).thenReturn(Stream.of(exporter));
37+
testCaseListener = Mockito.spy(new TestCaseListener(Mockito.mock(CatsGlobalContext.class), Mockito.mock(ExecutionStatisticsListener.class), exporters,
38+
Mockito.mock(IgnoreArguments.class), Mockito.mock(ReportingArguments.class)));
39+
multipleSuccessCodesLinterFuzzer = new MultipleSuccessCodesLinterFuzzer(testCaseListener);
40+
}
41+
42+
@Test
43+
void shouldReportWarn() throws Exception {
44+
OpenAPI openAPI = new OpenAPIParser().readContents(Files.readString(Paths.get("src/test/resources/inconsistent-api-2.yml")), null, null).getOpenAPI();
45+
FuzzingData data = FuzzingData.builder().openApi(openAPI).method(HttpMethod.PUT).pathItem(openAPI.getPaths().get("/users/{userId}")).build();
46+
multipleSuccessCodesLinterFuzzer.fuzz(data);
47+
48+
Mockito.verify(testCaseListener, Mockito.times(1)).reportResultWarn(Mockito.any(), Mockito.any(), Mockito.eq("Multiple 2xx success status codes found"), Mockito.eq("Operation defines multiple 2xx status codes: [200, 204]"));
49+
}
50+
51+
@Test
52+
void shouldReportInfo() throws Exception {
53+
OpenAPI openAPI = new OpenAPIParser().readContents(Files.readString(Paths.get("src/test/resources/consistent-api.yml")), null, null).getOpenAPI();
54+
FuzzingData data = FuzzingData.builder().openApi(openAPI).method(HttpMethod.POST).pathItem(openAPI.getPaths().get("/pets/states")).reqSchema(new Schema()).build();
55+
multipleSuccessCodesLinterFuzzer.fuzz(data);
56+
57+
Mockito.verify(testCaseListener, Mockito.times(1)).reportResultInfo(Mockito.any(), Mockito.any(), Mockito.eq("Each operation defines at most one success (2xx) response code"));
58+
}
59+
60+
@Test
61+
void shouldNotRunOnSecondAttempt() throws Exception {
62+
OpenAPI openAPI = new OpenAPIParser().readContents(Files.readString(Paths.get("src/test/resources/consistent-api.yml")), null, null).getOpenAPI();
63+
FuzzingData data = FuzzingData.builder().openApi(openAPI).pathItem(openAPI.getPaths().get("/pets/states")).method(HttpMethod.POST).reqSchema(new Schema()).build();
64+
multipleSuccessCodesLinterFuzzer.fuzz(data);
65+
66+
Mockito.verify(testCaseListener, Mockito.times(1)).reportResultInfo(Mockito.any(), Mockito.any(), Mockito.eq("Each operation defines at most one success (2xx) response code"));
67+
68+
Mockito.reset(testCaseListener);
69+
multipleSuccessCodesLinterFuzzer.fuzz(data);
70+
Mockito.verify(testCaseListener, Mockito.times(0)).createAndExecuteTest(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
71+
}
72+
73+
@Test
74+
void shouldReturnSimpleClassNameForToString() {
75+
Assertions.assertThat(multipleSuccessCodesLinterFuzzer).hasToString(multipleSuccessCodesLinterFuzzer.getClass().getSimpleName());
76+
}
77+
78+
@Test
79+
void shouldReturnMeaningfulDescription() {
80+
Assertions.assertThat(multipleSuccessCodesLinterFuzzer.description()).isEqualTo("flags operations that define more than one 2xx success response, which may cause ambiguity for SDKs and clients");
81+
}
82+
}

0 commit comments

Comments
 (0)