From 2d6240aab268c15e2f795d50b19376c1c7a092e2 Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Mon, 14 Apr 2025 16:47:22 +0200 Subject: [PATCH] Add a more complex CRUD example based on the Super Heroes Villain service This is mostly intended for (native) performance tracking --- crud-movie-quickstart/README.md | 20 ++ crud-movie-quickstart/pom.xml | 162 +++++++++++++++ .../src/main/java/org/acme/AppConfig.java | 22 ++ .../src/main/java/org/acme/Examples.java | 24 +++ .../src/main/java/org/acme/Movie.java | 18 ++ .../src/main/java/org/acme/MovieResource.java | 192 ++++++++++++++++++ .../java/org/acme/PingMovieHealthCheck.java | 22 ++ .../src/main/resources/application.properties | 35 ++++ .../src/main/resources/banner.txt | 11 + .../src/main/resources/import.sql | 12 ++ .../test/java/org/acme/MoviesEndpointIT.java | 171 ++++++++++++++++ .../java/org/acme/MoviesEndpointTest.java | 170 ++++++++++++++++ pom.xml | 13 ++ 13 files changed, 872 insertions(+) create mode 100644 crud-movie-quickstart/README.md create mode 100644 crud-movie-quickstart/pom.xml create mode 100644 crud-movie-quickstart/src/main/java/org/acme/AppConfig.java create mode 100644 crud-movie-quickstart/src/main/java/org/acme/Examples.java create mode 100644 crud-movie-quickstart/src/main/java/org/acme/Movie.java create mode 100644 crud-movie-quickstart/src/main/java/org/acme/MovieResource.java create mode 100644 crud-movie-quickstart/src/main/java/org/acme/PingMovieHealthCheck.java create mode 100644 crud-movie-quickstart/src/main/resources/application.properties create mode 100644 crud-movie-quickstart/src/main/resources/banner.txt create mode 100644 crud-movie-quickstart/src/main/resources/import.sql create mode 100644 crud-movie-quickstart/src/test/java/org/acme/MoviesEndpointIT.java create mode 100644 crud-movie-quickstart/src/test/java/org/acme/MoviesEndpointTest.java diff --git a/crud-movie-quickstart/README.md b/crud-movie-quickstart/README.md new file mode 100644 index 0000000000..1e0faecdea --- /dev/null +++ b/crud-movie-quickstart/README.md @@ -0,0 +1,20 @@ +# CRUD Movie Quickstart + +This is a simple CRUD application for managing movie rating. +It allows you to create, read, update, and delete movie rating records. + +You may find this example overly complicated for such a simple task. +It is intended to represent a more realistic application ready for production. + +The application uses the following extensions: + +- Quarkus REST (with Jackson) +- Hibernate ORM with Panache +- Hibernate Validator +- PostgreSQL +- SmallRye Health +- SmallRye OpenAPI +- Micrometer / Prometheus +- OpenTelemetry + +It uses virtual threads so, you need to run it with Java 21 or later. diff --git a/crud-movie-quickstart/pom.xml b/crud-movie-quickstart/pom.xml new file mode 100644 index 0000000000..e209a973ed --- /dev/null +++ b/crud-movie-quickstart/pom.xml @@ -0,0 +1,162 @@ + + + 4.0.0 + + org.acme + crud-movie-quickstart + 1.0.0-SNAPSHOT + + + quarkus-bom + io.quarkus + 999-SNAPSHOT + 3.11.0 + 3.1.2 + 21 + 21 + true + UTF-8 + + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + + + io.quarkus + quarkus-rest-jackson + + + io.quarkus + quarkus-jdbc-postgresql + + + io.quarkus + quarkus-hibernate-orm-panache + + + io.quarkus + quarkus-hibernate-validator + + + io.quarkus + quarkus-smallrye-openapi + + + io.quarkus + quarkus-opentelemetry + + + io.opentelemetry.instrumentation + opentelemetry-jdbc + + + io.quarkus + quarkus-micrometer-registry-prometheus + + + io.quarkus + quarkus-smallrye-health + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${compiler-plugin.version} + + ${maven.compiler.parameters} + + + + org.apache.maven.plugins + maven-surefire-plugin + ${surefire-plugin.version} + + + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + native + + + native + + + + + + maven-failsafe-plugin + ${surefire-plugin.version} + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + true + false + + + + diff --git a/crud-movie-quickstart/src/main/java/org/acme/AppConfig.java b/crud-movie-quickstart/src/main/java/org/acme/AppConfig.java new file mode 100644 index 0000000000..9ac7a1ae2f --- /dev/null +++ b/crud-movie-quickstart/src/main/java/org/acme/AppConfig.java @@ -0,0 +1,22 @@ +package org.acme; + +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +import java.util.Optional; + +/** + * Configuration class for the movies prefix. + */ +@ConfigMapping(prefix = "movies") +public interface AppConfig { + + + Hello hello(); + + interface Hello { + + @WithDefault("Hello Movie") + String message(); + } +} \ No newline at end of file diff --git a/crud-movie-quickstart/src/main/java/org/acme/Examples.java b/crud-movie-quickstart/src/main/java/org/acme/Examples.java new file mode 100644 index 0000000000..24a7a55755 --- /dev/null +++ b/crud-movie-quickstart/src/main/java/org/acme/Examples.java @@ -0,0 +1,24 @@ +package org.acme; + +final class Examples { + private Examples() { + + } + + static final String VALID_EXAMPLE_MOVIE = """ + { + "id": 1, + "name": "Inception", + "rating": 5 + } + """; + + static final String VALID_EXAMPLE_MOVIE_TO_CREATE = """ + { + "name": "Inception", + "rating": 4 + } + """; + + static final String VALID_EXAMPLE_MOVIE_LIST = "[" + VALID_EXAMPLE_MOVIE + "]"; +} \ No newline at end of file diff --git a/crud-movie-quickstart/src/main/java/org/acme/Movie.java b/crud-movie-quickstart/src/main/java/org/acme/Movie.java new file mode 100644 index 0000000000..bf24c91adb --- /dev/null +++ b/crud-movie-quickstart/src/main/java/org/acme/Movie.java @@ -0,0 +1,18 @@ +package org.acme; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import jakarta.persistence.Entity; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +@Entity +public class Movie extends PanacheEntity { + + @NotBlank + public String title; + + @Min(0) @Max(5) + public int rating; + +} diff --git a/crud-movie-quickstart/src/main/java/org/acme/MovieResource.java b/crud-movie-quickstart/src/main/java/org/acme/MovieResource.java new file mode 100644 index 0000000000..75f7cc7637 --- /dev/null +++ b/crud-movie-quickstart/src/main/java/org/acme/MovieResource.java @@ -0,0 +1,192 @@ +package org.acme; + +import io.quarkus.logging.Log; +import io.smallrye.common.annotation.NonBlocking; +import io.smallrye.common.annotation.RunOnVirtualThread; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.PATCH; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.headers.Header; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.ExampleObject; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +import java.net.URI; +import java.util.List; + +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; + +@Path("/movies") +@Tag(name = "movies") +@RunOnVirtualThread +public class MovieResource { + + @GET + @Operation(summary = "Returns all the movies stored in the database") + @APIResponse( + responseCode = "200", + description = "Gets all movies", + content = @Content( + mediaType = APPLICATION_JSON, + schema = @Schema(implementation = Movie.class, type = SchemaType.ARRAY), + examples = @ExampleObject(name = "movies", value = Examples.VALID_EXAMPLE_MOVIE_LIST) + ) + ) + public List getAll() { + return Movie.listAll(); + } + + @GET + @Path("/{id}") + @Operation(summary = "Returns a movie for a given identifier") + @APIResponse( + responseCode = "200", + description = "Gets a movie for a given id", + content = @Content( + mediaType = APPLICATION_JSON, + schema = @Schema(implementation = Movie.class), + examples = @ExampleObject(name = "movie", value = Examples.VALID_EXAMPLE_MOVIE) + ) + ) + @APIResponse( + responseCode = "404", + description = "The movie is not found for a given identifier" + ) + public Movie getOne(Long id) { + Movie movie = Movie.findById(id); + if (movie == null) { + throw new NotFoundException("Movie not found"); + } + return movie; + } + + @POST + @Transactional + @Operation(summary = "Adds a movie and its associated rating") + @APIResponse( + responseCode = "201", + description = "The URI of the created movie", + headers = @Header(name = HttpHeaders.LOCATION, schema = @Schema(implementation = URI.class)) + ) + @APIResponse( + responseCode = "400", + description = "Invalid movie passed in (or no request body found)" + ) + public Response create( + @RequestBody( + name = "movie", + content = @Content( + mediaType = APPLICATION_JSON, + schema = @Schema(implementation = Movie.class), + examples = @ExampleObject(name = "movie", value = Examples.VALID_EXAMPLE_MOVIE_TO_CREATE) + ) + ) + @Valid @NotNull Movie movie, + @Context UriInfo uriInfo) { + movie.persist(); + var builder = uriInfo.getAbsolutePathBuilder().path(Long.toString(movie.id)); + Log.debugf("New movie created with URI %s", builder.build().toString()); + return Response.created(builder.build()).build(); + } + + @PATCH + @Path("/{id}") + @Transactional + @Operation(summary = "Updates an exiting movie") + @APIResponse( + responseCode = "200", + description = "Movie updated", + content = @Content( + mediaType = APPLICATION_JSON, + schema = @Schema(implementation = Movie.class), + examples = @ExampleObject(name = "movie", value = Examples.VALID_EXAMPLE_MOVIE) + ) + ) + @APIResponse( + responseCode = "400", + description = "Invalid movie passed in (or no request body found)") + @APIResponse( + responseCode = "404", + description = "The movie is not found for a given identifier" + ) + public Movie update(@Parameter(name = "id", required = true) Long id, + @RequestBody( + name = "movie", + content = @Content( + schema = @Schema(implementation = Movie.class), + examples = @ExampleObject(name = "movie", value = Examples.VALID_EXAMPLE_MOVIE) + ) + ) + @NotNull Movie movie) { + Movie existing = Movie.findById(id); + if (existing == null) { + throw new NotFoundException("Movie not found"); + } + existing.title = movie.title; + existing.rating = movie.rating; + return existing; + } + + @DELETE + @Path("/{id}") + @Transactional + @Operation(summary = "Deletes an exiting movie") + public void delete(@Parameter(name = "id", required = true) Long id) { + Movie movie = Movie.findById(id); + if (movie == null) { + throw new NotFoundException("Movie not found"); + } + Log.debugf("Movie with id %d deleted ", id); + movie.delete(); + } + + @DELETE + @Transactional + @Operation(summary = "Delete all movies") + @APIResponse( + responseCode = "204", + description = "Deletes all movies" + ) + public void deleteAll() { + Movie.deleteAll(); + Log.debug("Deleted all movies"); + + } + + @Inject + AppConfig config; + + @GET + @Path("/hello") + @Tag(name = "hello") + @Operation(summary = "Ping hello") + @APIResponse( + responseCode = "200", + description = "Ping hello", + content = @Content( + schema = @Schema(implementation = String.class), + examples = @ExampleObject(name = "hello", value = "Hello Movie Resource") + ) + ) + public String hello() { + Log.debugf("Hello Movie Resource - returning %s", config.hello().message()); + return config.hello().message(); + } +} diff --git a/crud-movie-quickstart/src/main/java/org/acme/PingMovieHealthCheck.java b/crud-movie-quickstart/src/main/java/org/acme/PingMovieHealthCheck.java new file mode 100644 index 0000000000..89d46e5fdc --- /dev/null +++ b/crud-movie-quickstart/src/main/java/org/acme/PingMovieHealthCheck.java @@ -0,0 +1,22 @@ +package org.acme; + +import jakarta.inject.Inject; +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.Liveness; + +@Liveness +public class PingMovieHealthCheck implements HealthCheck { + @Inject + MovieResource resource; + + @Override + public HealthCheckResponse call() { + var response = resource.hello(); + + return HealthCheckResponse.named("Ping Movie REST Endpoint") + .withData("Response", response) + .up() + .build(); + } +} \ No newline at end of file diff --git a/crud-movie-quickstart/src/main/resources/application.properties b/crud-movie-quickstart/src/main/resources/application.properties new file mode 100644 index 0000000000..a61e708d6b --- /dev/null +++ b/crud-movie-quickstart/src/main/resources/application.properties @@ -0,0 +1,35 @@ +quarkus.application.name=movies-service +quarkus.banner.path=banner.txt + +quarkus.jackson.serialization-inclusion=non-empty + +quarkus.hibernate-orm.database.generation=drop-and-create + +movies.hello.message=OK + +## Logging configuration +quarkus.log.category."org.acme".level=DEBUG +quarkus.log.level=INFO +%dev,test.quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] (%t) %s%e%n +quarkus.log.console.format=%d{HH:mm:ss} %-5p traceId=%X{traceId}, parentId=%X{parentId}, spanId=%X{spanId}, sampled=%X{sampled} [%c{2.}] (%t) %s%e%n +quarkus.log.console.level=INFO +quarkus.log.console.darken=1 +%dev,test.quarkus.log.console.level=DEBUG + +# OpenTelemetry +quarkus.otel.resource.attributes=app=${quarkus.application.name},application=movies-service +quarkus.datasource.jdbc.telemetry=true + +## CORS +quarkus.http.cors.enabled=true +quarkus.http.cors.origins=* + +# OpenAPI +quarkus.smallrye-openapi.info-title=Movie Rating API +quarkus.smallrye-openapi.info-description=This API allows CRUD operations on a movie ratings +quarkus.smallrye-openapi.info-version=1.0 +quarkus.smallrye-openapi.info-contact-name=Quarkus +quarkus.smallrye-openapi.info-contact-url=https://github.com/quarkusio +quarkus.swagger-ui.always-include=true + + diff --git a/crud-movie-quickstart/src/main/resources/banner.txt b/crud-movie-quickstart/src/main/resources/banner.txt new file mode 100644 index 0000000000..ea21c16263 --- /dev/null +++ b/crud-movie-quickstart/src/main/resources/banner.txt @@ -0,0 +1,11 @@ +888b d888 d8b 888b 888 d8b 888 888 888 +8888b d8888 Y8P 8888b 888 Y8P 888 888 888 +88888b.d88888 88888b 888 888 888 888 +888Y88888P888 .d88b. 888 888 888 .d88b. 888Y88b 888 888 .d88b. 88888b. 888888 888 +888 Y888P 888 d88""88b 888 888 888 d8P Y8b 888 Y88b888 888 d88P"88b 888 "88b 888 888 +888 Y8P 888 888 888 Y88 88P 888 88888888 888 Y88888 888 888 888 888 888 888 Y8P +888 " 888 Y88..88P Y8bd8P 888 Y8b. 888 Y8888 888 Y88b 888 888 888 Y88b. " +888 888 "Y88P" Y88P 888 "Y8888 888 Y888 888 "Y88888 888 888 "Y888 888 + 888 + Y8b d88P + "Y88P" \ No newline at end of file diff --git a/crud-movie-quickstart/src/main/resources/import.sql b/crud-movie-quickstart/src/main/resources/import.sql new file mode 100644 index 0000000000..46573982f8 --- /dev/null +++ b/crud-movie-quickstart/src/main/resources/import.sql @@ -0,0 +1,12 @@ +ALTER SEQUENCE movie_seq RESTART WITH 50; + +INSERT INTO Movie (id, title, rating) VALUES (nextval('movie_seq'), 'The Matrix', 5); +INSERT INTO Movie (id, title, rating) VALUES (nextval('movie_seq'), 'Inception', 5); +INSERT INTO Movie (id, title, rating) VALUES (nextval('movie_seq'), 'Interstellar', 4); +INSERT INTO Movie (id, title, rating) VALUES (nextval('movie_seq'), 'The Godfather', 5); +INSERT INTO Movie (id, title, rating) VALUES (nextval('movie_seq'), 'Pulp Fiction', 5); +INSERT INTO Movie (id, title, rating) VALUES (nextval('movie_seq'), 'The Shawshank Redemption', 5); +INSERT INTO Movie (id, title, rating) VALUES (nextval('movie_seq'), 'Fight Club', 4); +INSERT INTO Movie (id, title, rating) VALUES (nextval('movie_seq'), 'Forrest Gump', 5); +INSERT INTO Movie (id, title, rating) VALUES (nextval('movie_seq'), 'The Dark Knight', 5); +INSERT INTO Movie (id, title, rating) VALUES (nextval('movie_seq'), 'The Lord of the Rings', 5); \ No newline at end of file diff --git a/crud-movie-quickstart/src/test/java/org/acme/MoviesEndpointIT.java b/crud-movie-quickstart/src/test/java/org/acme/MoviesEndpointIT.java new file mode 100644 index 0000000000..bf3521904c --- /dev/null +++ b/crud-movie-quickstart/src/test/java/org/acme/MoviesEndpointIT.java @@ -0,0 +1,171 @@ +package org.acme; + +import io.quarkus.test.junit.QuarkusIntegrationTest; +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; + +@QuarkusIntegrationTest +public class MoviesEndpointIT { + + @BeforeEach + public void setUp() { + given() + .when().delete("/movies") + .then() + .statusCode(204); + } + + @Test + public void testGetAllInitiallyEmpty() { + given() + .when().get("/movies") + .then() + .statusCode(200) + .body("$.size()", is(0)); + } + + @Test + public void testCreateMovie() { + var uri = given() + .contentType(JSON) + .body("{\"title\":\"Inception\",\"rating\":5}") + .when().post("/movies") + .then() + .statusCode(201) + .extract().header("Location"); + + given() + .get(uri) + .then() + .body("id", notNullValue()) + .body("title", is("Inception")) + .body("rating", is(5)); + } + + @Test + public void testGetOneAndNotFound() { + var uri = given() + .contentType(JSON) + .body("{\"title\":\"Interstellar\",\"rating\":4}") + .when().post("/movies") + .then() + .statusCode(201) + .extract().header("Location"); + + given() + .when().get(uri) + .then() + .statusCode(200) + .body("title", is("Interstellar")); + + given() + .when().get("/movies/9999") + .then() + .statusCode(404); + } + + @Test + public void testUpdateMovie() { + var uri = given() + .contentType(JSON) + .body("{\"title\":\"Old Title\",\"rating\":2}") + .when().post("/movies") + .then() + .statusCode(201) + .extract().header("Location"); + + given() + .contentType(JSON) + .body("{\"title\":\"New Title\",\"rating\":3}") + .when().patch(uri) + .then() + .statusCode(200) + .body("title", is("New Title")) + .body("rating", is(3)); + } + + @Test + public void testUpdateNotFound() { + given() + .contentType(JSON) + .body("{\"title\":\"Doesn't Matter\",\"rating\":1}") + .when().patch("/movies/9999") + .then() + .statusCode(404); + } + + @Test + public void testDeleteMovie() { + var uri = given() + .contentType(JSON) + .body("{\"title\":\"To be deleted\",\"rating\":1}") + .when().post("/movies") + .then() + .statusCode(201) + .extract().header("Location"); + + given() + .when().delete(uri) + .then() + .statusCode(204); + + given() + .when().get(uri) + .then() + .statusCode(404); + } + + @Test + public void testDeleteNotFound() { + given() + .when().delete("/movies/9999") + .then() + .statusCode(404); + } + + @Test + void helloEndpoint() { + given() + .get("/movies/hello") + .then() + .statusCode(200) + .contentType("text/plain") + .body(is("OK")); + } + + @Test + void shouldNotAddInvalidItem() { + var movie = new Movie(); + movie.title = null; + movie.rating = 2; + + given() + .when() + .body(movie) + .contentType(JSON) + .accept(JSON) + .post("/movies") + .then() + .statusCode(400); + } + + @Test + void shouldNotAddNullItem() { + given() + .when() + .contentType(JSON) + .accept(JSON) + .post("/movies") + .then() + .statusCode(400); + } + + + +} \ No newline at end of file diff --git a/crud-movie-quickstart/src/test/java/org/acme/MoviesEndpointTest.java b/crud-movie-quickstart/src/test/java/org/acme/MoviesEndpointTest.java new file mode 100644 index 0000000000..ad582c3398 --- /dev/null +++ b/crud-movie-quickstart/src/test/java/org/acme/MoviesEndpointTest.java @@ -0,0 +1,170 @@ +package org.acme; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; + +@QuarkusTest +public class MoviesEndpointTest { + + @BeforeEach + public void setUp() { + given() + .when().delete("/movies") + .then() + .statusCode(204); + } + + @Test + public void testGetAllInitiallyEmpty() { + given() + .when().get("/movies") + .then() + .statusCode(200) + .body("$.size()", is(0)); + } + + @Test + public void testCreateMovie() { + var uri = given() + .contentType(JSON) + .body("{\"title\":\"Inception\",\"rating\":5}") + .when().post("/movies") + .then() + .statusCode(201) + .extract().header("Location"); + + given() + .get(uri) + .then() + .body("id", notNullValue()) + .body("title", is("Inception")) + .body("rating", is(5)); + } + + @Test + public void testGetOneAndNotFound() { + var uri = given() + .contentType(JSON) + .body("{\"title\":\"Interstellar\",\"rating\":4}") + .when().post("/movies") + .then() + .statusCode(201) + .extract().header("Location"); + + given() + .when().get(uri) + .then() + .statusCode(200) + .body("title", is("Interstellar")); + + given() + .when().get("/movies/9999") + .then() + .statusCode(404); + } + + @Test + public void testUpdateMovie() { + var uri = given() + .contentType(JSON) + .body("{\"title\":\"Old Title\",\"rating\":2}") + .when().post("/movies") + .then() + .statusCode(201) + .extract().header("Location"); + + given() + .contentType(JSON) + .body("{\"title\":\"New Title\",\"rating\":3}") + .when().patch(uri) + .then() + .statusCode(200) + .body("title", is("New Title")) + .body("rating", is(3)); + } + + @Test + public void testUpdateNotFound() { + given() + .contentType(JSON) + .body("{\"title\":\"Doesn't Matter\",\"rating\":1}") + .when().patch("/movies/9999") + .then() + .statusCode(404); + } + + @Test + public void testDeleteMovie() { + var uri = given() + .contentType(JSON) + .body("{\"title\":\"To be deleted\",\"rating\":1}") + .when().post("/movies") + .then() + .statusCode(201) + .extract().header("Location"); + + given() + .when().delete(uri) + .then() + .statusCode(204); + + given() + .when().get(uri) + .then() + .statusCode(404); + } + + @Test + public void testDeleteNotFound() { + given() + .when().delete("/movies/9999") + .then() + .statusCode(404); + } + + @Test + void helloEndpoint() { + given() + .get("/movies/hello") + .then() + .statusCode(200) + .contentType("text/plain") + .body(is("OK")); + } + + @Test + void shouldNotAddInvalidItem() { + var movie = new Movie(); + movie.title = null; + movie.rating = 2; + + given() + .when() + .body(movie) + .contentType(JSON) + .accept(JSON) + .post("/movies") + .then() + .statusCode(400); + } + + @Test + void shouldNotAddNullItem() { + given() + .when() + .contentType(JSON) + .accept(JSON) + .post("/movies") + .then() + .statusCode(400); + } + + + +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 4d43cf5e1b..f5d6e86b76 100644 --- a/pom.xml +++ b/pom.xml @@ -120,6 +120,7 @@ jta-quickstart + @@ -129,4 +130,16 @@ + + + + java-21+ + + [21,) + + + crud-movie-quickstart + + +