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
+
+
+