Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BAEL 8916 - Automated Testing for OpenAPI Endpoints Using CATS #18056

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions spring-boot-modules/spring-boot-springdoc-2/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
Expand Down Expand Up @@ -58,6 +71,9 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.baeldung.springdoc.SpringdocApplication</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.baeldung.cats;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityScheme;

@SpringBootApplication
@OpenAPIDefinition(info = @Info(title = "Automated Testing for OpenAPI Endpoints Using CATS", version = "1.0.0"), security = { @SecurityRequirement(name = "Authorization") })
@SecurityScheme(type = SecuritySchemeType.HTTP, name = "Authorization", in = SecuritySchemeIn.HEADER, scheme = "Bearer")
public class CatsApplication {

public static void main(String[] args) {
SpringApplication.run(CatsApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.baeldung.cats.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonFactoryBuilder;
import com.fasterxml.jackson.core.StreamReadConstraints;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;

@Configuration
public class JacksonConfig {

@Bean
public ObjectMapper objectMapper() {
JsonFactory factory = new JsonFactoryBuilder().streamReadConstraints(StreamReadConstraints.builder()
.maxStringLength(100)
.build())
.build();

ObjectMapper mapper = new ObjectMapper(factory);
mapper.enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);
mapper.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

mapper.disable(DeserializationFeature.ACCEPT_FLOAT_AS_INT);
mapper.enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES);
return mapper;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.baeldung.cats.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

private static final String TEST_TOKEN = "valid-token";
private static final String ACCESS_DENIED = "access denied";

private final Logger log = LoggerFactory.getLogger(SecurityConfig.class);

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.authorizeHttpRequests(auth -> auth.requestMatchers("/swagger-ui-custom.html", "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", "/webjars/**", "/swagger-ui/index.html", "/api-docs/**")
.permitAll()
.anyRequest()
.authenticated())
.oauth2ResourceServer(rs -> rs.jwt(jwt -> jwt.decoder(jwtDecoder())))
.build();
}

@Bean
JwtDecoder jwtDecoder() {
return token -> {
if (!TEST_TOKEN.equals(token)) {
log.warn(ACCESS_DENIED);
throw new JwtException(ACCESS_DENIED);
}

return Jwt.withTokenValue(token)
.claim("token", TEST_TOKEN)
.header("token", TEST_TOKEN)
.build();
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.baeldung.cats.config;

import java.io.IOException;

import com.baeldung.cats.utils.RegexUtils;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;

public class ZeroWidthIntDeserializer extends JsonDeserializer<Integer> {

@Override
public Integer deserialize(JsonParser parser, DeserializationContext context) throws IOException {
return Integer.valueOf(RegexUtils.removeZeroWidthChars(parser.getText()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.baeldung.cats.config;

import java.io.IOException;

import com.baeldung.cats.utils.RegexUtils;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;

public class ZeroWidthStringDeserializer extends JsonDeserializer<String> {

@Override
public String deserialize(JsonParser parser, DeserializationContext context) throws IOException {
return RegexUtils.removeZeroWidthChars(parser.getText());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.baeldung.cats.controller;

import java.util.List;

import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.baeldung.cats.model.BadApiRequest;
import com.baeldung.cats.model.Item;
import com.baeldung.cats.service.ItemService;

import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.persistence.EntityNotFoundException;
import jakarta.validation.Valid;

@RestController
@RequestMapping("/api/v2/item")
@ApiResponse(responseCode = "401", description = "Unauthorized", content = { @Content(mediaType = MediaType.TEXT_PLAIN_VALUE, schema = @Schema(implementation = String.class)) })
@ApiResponse(responseCode = "400", description = "Bad Request", content = { @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = BadApiRequest.class)) })
public class ItemController {

private ItemService service;

public ItemController(ItemService service) {
this.service = service;
}

@PostMapping
@ApiResponse(responseCode = "200", description = "Success", content = { @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = Item.class)) })
public ResponseEntity<Item> post(@Valid @RequestBody Item item) {
service.insert(item);
return ResponseEntity.ok(item);
}

@GetMapping
@ApiResponse(responseCode = "200", description = "Success", content = { @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, array = @ArraySchema(schema = @Schema(implementation = Item.class))) })
public ResponseEntity<List<Item>> get() {
return ResponseEntity.ok(service.findAll());
}

@GetMapping("/{id}")
@ApiResponse(responseCode = "200", description = "Success", content = { @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = Item.class)) })
public ResponseEntity<Item> get(@PathVariable String id) {
try {
return ResponseEntity.ok(service.findById(id));
} catch (EntityNotFoundException e) {
return ResponseEntity.status(404)
.build();
}
}

@DeleteMapping("/{id}")
@ApiResponse(responseCode = "204", description = "Success")
public ResponseEntity<Void> delete(@PathVariable String id) {
try {
service.deleteById(id);

return ResponseEntity.status(204)
.build();
} catch (EntityNotFoundException e) {
return ResponseEntity.status(404)
.build();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.baeldung.cats.model;

public class BadApiRequest {

private long timestamp;
private int status;
private String error;
private String path;

public long getTimestamp() {
return timestamp;
}

public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}

public int getStatus() {
return status;
}

public void setStatus(int status) {
this.status = status;
}

public String getError() {
return error;
}

public void setError(String error) {
this.error = error;
}

public String getPath() {
return path;
}

public void setPath(String path) {
this.path = path;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.baeldung.cats.model;

import java.util.Objects;

import com.baeldung.cats.config.ZeroWidthIntDeserializer;
import com.baeldung.cats.config.ZeroWidthStringDeserializer;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public class Item {

@Size(min = 37, max = 37)
@JsonDeserialize(using = ZeroWidthStringDeserializer.class)
private String id;

@NotNull
@Size(min = 1, max = 20)
@JsonDeserialize(using = ZeroWidthStringDeserializer.class)
private String name;

@Min(1)
@Max(100)
@NotNull
@JsonDeserialize(using = ZeroWidthIntDeserializer.class)
private int value;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getValue() {
return value;
}

public void setValue(int value) {
this.value = value;
}

@Override
public String toString() {
return "Item [id=" + id + ", name=" + name + ", value=" + value + "]";
}

@Override
public int hashCode() {
return Objects.hash(id);
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Item other = (Item) obj;
return Objects.equals(id, other.id);
}
}
Loading