diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..f8102bca --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,11 @@ +# Not actually used by the devcontainer, but it is used by gitpod +ARG VARIANT=17-bullseye +FROM mcr.microsoft.com/vscode/devcontainers/java:0-${VARIANT} +ARG NODE_VERSION="none" +RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi +ARG USER=vscode +VOLUME /home/$USER/.m2 +VOLUME /home/$USER/.gradle +ARG JAVA_VERSION=17.0.7-ms +RUN sudo mkdir /home/$USER/.m2 /home/$USER/.gradle && sudo chown $USER:$USER /home/$USER/.m2 /home/$USER/.gradle +RUN bash -lc '. /usr/local/sdkman/bin/sdkman-init.sh && sdk install java $JAVA_VERSION && sdk use java $JAVA_VERSION' \ No newline at end of file diff --git a/.github/workflows/deploy-and-test-cluster.yml b/.github/workflows/deploy-and-test-cluster.yml new file mode 100644 index 00000000..7353a604 --- /dev/null +++ b/.github/workflows/deploy-and-test-cluster.yml @@ -0,0 +1,31 @@ +name: Deploy and Test Cluster + +on: + push: + branches: [main] + paths: + - 'k8s/**' + pull_request: + branches: [main] + paths: + - 'k8s/**' + +jobs: + deploy-and-test-cluster: + runs-on: ubuntu-latest + steps: + - name: Check out the repository + uses: actions/checkout@v2 + + - name: Create k8s Kind Cluster + uses: helm/kind-action@v1 + + - name: Deploy application + run: | + kubectl apply -f k8s/ + + - name: Wait for Pods to be ready + run: | + kubectl wait --for=condition=ready pod -l app=demo-db --timeout=180s + kubectl wait --for=condition=ready pod -l app=petclinic --timeout=180s + diff --git a/.github/workflows/gradle-build.yml b/.github/workflows/gradle-build.yml new file mode 100644 index 00000000..c24c121b --- /dev/null +++ b/.github/workflows/gradle-build.yml @@ -0,0 +1,31 @@ +# This workflow will build a Java project with Gradle, and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-java-with-gradle + +name: Java CI with Gradle + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + java: [ '17' ] + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK ${{matrix.java}} + uses: actions/setup-java@v4 + with: + java-version: ${{matrix.java}} + distribution: 'adopt' + cache: maven + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + - name: Build with Gradle + run: ./gradlew build diff --git a/.github/workflows/maven-build.yml b/.github/workflows/maven-build.yml index 4718a6ce..a1ec4dab 100644 --- a/.github/workflows/maven-build.yml +++ b/.github/workflows/maven-build.yml @@ -1,5 +1,5 @@ # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time -# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven +# For more information see: https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-java-with-maven name: Java CI with Maven @@ -26,4 +26,4 @@ jobs: distribution: 'adopt' cache: maven - name: Build with Maven Wrapper - run: ./mvnw -B package + run: ./mvnw -B verify diff --git a/.gitignore b/.gitignore index d04d6853..7ff73961 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,50 @@ -target/* -bin/* -build/* -.gradle/* -.settings/* +HELP.md +pom.xml.bak +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.attach_pid* +.apt_generated .classpath -.project .factorypath -.attach_pid* +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### .idea +*.iws *.iml -/target -.sts4-cache/ -.vscode +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### SDK Man ### +.sdkmanrc + +### CSS ### _site/ **/.DS_Store diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index ce65fef7..654af46a 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,3 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. wrapperVersion=3.3.2 distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/readme.md b/README.md similarity index 96% rename from readme.md rename to README.md index 6c2c9497..1d77167d 100644 --- a/readme.md +++ b/README.md @@ -1,4 +1,4 @@ -# Spring PetClinic Sample Application [![Build Status](https://github.com/spring-petclinic/spring-petclinic-ai/actions/workflows/maven-build.yml/badge.svg)](https://github.com/spring-petclinic/spring-petclinic-ai/actions/workflows/maven-build.yml) +# Spring PetClinic Sample Application [![Build Status](https://github.com/spring-petclinic/spring-petclinic-ai/actions/workflows/maven-build.yml/badge.svg)](https://github.com/spring-petclinic/spring-petclinic-ai/actions/workflows/maven-build.yml)[![Build Status](https://github.com/spring-petclinic/spring-petclinic-ai/actions/workflows/gradle-build.yml/badge.svg)](https://github.com/spring-petclinic/spring-petclinic-ai/actions/workflows/gradle-build.yml) [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/spring-petclinic/spring-petclinic-ai) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=7517918) @@ -84,28 +84,28 @@ A similar setup is provided for MySQL and PostgreSQL if a persistent database co You can start MySQL or PostgreSQL locally with whatever installer works for your OS or use docker: ```bash -docker run -e MYSQL_USER=petclinic -e MYSQL_PASSWORD=petclinic -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=petclinic -p 3306:3306 mysql:8.4 +docker run -e MYSQL_USER=petclinic -e MYSQL_PASSWORD=petclinic -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=petclinic -p 3306:3306 mysql:9.1 ``` or ```bash -docker run -e POSTGRES_USER=petclinic -e POSTGRES_PASSWORD=petclinic -e POSTGRES_DB=petclinic -p 5432:5432 postgres:16.3 +docker run -e POSTGRES_USER=petclinic -e POSTGRES_PASSWORD=petclinic -e POSTGRES_DB=petclinic -p 5432:5432 postgres:17.0 ``` Further documentation is provided for [MySQL](https://github.com/spring-petclinic/spring-petclinic-ai/blob/main/src/main/resources/db/mysql/petclinic_db_setup_mysql.txt) and [PostgreSQL](https://github.com/spring-petclinic/spring-petclinic-ai/blob/main/src/main/resources/db/postgres/petclinic_db_setup_postgres.txt). -Instead of vanilla `docker` you can also use the provided `docker-compose.yml` file to start the database containers. Each one has a profile just like the Spring profile: +Instead of vanilla `docker` you can also use the provided `docker-compose.yml` file to start the database containers. Each one has a service named after the Spring profile: ```bash -docker-compose --profile mysql up +docker compose up mysql ``` or ```bash -docker-compose --profile postgres up +docker compose up postgres ``` ## Test Applications diff --git a/build.gradle b/build.gradle index b05a3e64..758cb6eb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,10 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.3.2' - id 'io.spring.dependency-management' version '1.1.5' - id 'org.graalvm.buildtools.native' version '0.10.2' - id 'org.cyclonedx.bom' version '1.8.2' - id 'io.spring.javaformat' version '0.0.41' + id 'org.springframework.boot' version '3.4.0' + id 'io.spring.dependency-management' version '1.1.6' + id 'org.graalvm.buildtools.native' version '0.10.3' + id 'org.cyclonedx.bom' version '1.10.0' + id 'io.spring.javaformat' version '0.0.43' id "io.spring.nohttp" version "0.0.11" } @@ -15,7 +15,7 @@ apply plugin: 'io.spring.javaformat' gradle.startParameter.excludedTaskNames += [ "checkFormatAot", "checkFormatAotTest" ] group = 'org.springframework.samples' -version = '3.3.0' +version = '3.4.0' java { sourceCompatibility = JavaVersion.VERSION_17 @@ -26,12 +26,18 @@ repositories { maven { url 'https://repo.spring.io/milestone' } } +ext.checkstyleVersion = "10.20.1" +ext.springJavaformatCheckstyleVersion = "0.0.43" +ext.webjarsLocatorLiteVersion = "1.0.1" ext.webjarsFontawesomeVersion = "4.7.0" ext.webjarsBootstrapVersion = "5.3.3" ext.webjarsMarkedVersion = "14.1.2" ext.springAiVersion = "1.0.0-M2" dependencies { + // Workaround for AOT issue (https://github.com/spring-projects/spring-framework/pull/33949) --> + implementation 'io.projectreactor:reactor-core' + implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-cache' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' @@ -41,6 +47,7 @@ dependencies { implementation 'javax.cache:cache-api' implementation 'jakarta.xml.bind:jakarta.xml.bind-api' runtimeOnly 'org.springframework.boot:spring-boot-starter-actuator' + runtimeOnly "org.webjars:webjars-locator-lite:${webjarsLocatorLiteVersion}" runtimeOnly "org.webjars.npm:bootstrap:${webjarsBootstrapVersion}" runtimeOnly "org.webjars.npm:font-awesome:${webjarsFontawesomeVersion}" runtimeOnly "org.webjars.npm:marked:${webjarsMarkedVersion}" @@ -54,8 +61,8 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-docker-compose' testImplementation 'org.testcontainers:junit-jupiter' testImplementation 'org.testcontainers:mysql' - checkstyle 'io.spring.javaformat:spring-javaformat-checkstyle:0.0.41' - checkstyle 'com.puppycrawl.tools:checkstyle:10.16.0' + checkstyle "io.spring.javaformat:spring-javaformat-checkstyle:${springJavaformatCheckstyleVersion}" + checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" } dependencyManagement { diff --git a/docker-compose.yml b/docker-compose.yml index aaebf7ca..47579bba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: mysql: - image: mysql:8.4 + image: mysql:9.1 ports: - "3306:3306" environment: @@ -11,15 +11,11 @@ services: - MYSQL_DATABASE=petclinic volumes: - "./conf.d:/etc/mysql/conf.d:ro" - profiles: - - mysql postgres: - image: postgres:16.3 + image: postgres:17.0 ports: - "5432:5432" environment: - POSTGRES_PASSWORD=petclinic - POSTGRES_USER=petclinic - POSTGRES_DB=petclinic - profiles: - - postgres diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e6441136..a4b76b95 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b82aa23a..df97d72b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a42..f5feea6d 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 25da30db..9d21a218 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## diff --git a/k8s/db.yml b/k8s/db.yml new file mode 100644 index 00000000..c230ddba --- /dev/null +++ b/k8s/db.yml @@ -0,0 +1,73 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: demo-db +type: servicebinding.io/postgresql +stringData: + type: "postgresql" + provider: "postgresql" + host: "demo-db" + port: "5432" + database: "petclinic" + username: "user" + password: "pass" + +--- +apiVersion: v1 +kind: Service +metadata: + name: demo-db +spec: + ports: + - port: 5432 + selector: + app: demo-db + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: demo-db + labels: + app: demo-db +spec: + selector: + matchLabels: + app: demo-db + template: + metadata: + labels: + app: demo-db + spec: + containers: + - image: postgres:17 + name: postgresql + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: demo-db + key: username + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: demo-db + key: password + - name: POSTGRES_DB + valueFrom: + secretKeyRef: + name: demo-db + key: database + ports: + - containerPort: 5432 + name: postgresql + livenessProbe: + tcpSocket: + port: postgresql + readinessProbe: + tcpSocket: + port: postgresql + startupProbe: + tcpSocket: + port: postgresql diff --git a/k8s/petclinic.yml b/k8s/petclinic.yml new file mode 100644 index 00000000..a5677cd0 --- /dev/null +++ b/k8s/petclinic.yml @@ -0,0 +1,64 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: petclinic +spec: + type: NodePort + ports: + - port: 80 + targetPort: 8080 + selector: + app: petclinic + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: petclinic + labels: + app: petclinic +spec: + replicas: 1 + selector: + matchLabels: + app: petclinic + template: + metadata: + labels: + app: petclinic + spec: + containers: + - name: workload + image: dsyer/petclinic + env: + - name: SPRING_PROFILES_ACTIVE + value: postgres + - name: SERVICE_BINDING_ROOT + value: /bindings + - name: SPRING_APPLICATION_JSON + value: | + { + "management.endpoint.health.probes.add-additional-paths": true + } + ports: + - name: http + containerPort: 8080 + livenessProbe: + httpGet: + path: /livez + port: http + readinessProbe: + httpGet: + path: /readyz + port: http + volumeMounts: + - mountPath: /bindings/secret + name: binding + readOnly: true + volumes: + - name: binding + projected: + sources: + - secret: + name: demo-db diff --git a/pom.xml b/pom.xml index 32036b31..e77cdea4 100644 --- a/pom.xml +++ b/pom.xml @@ -1,17 +1,18 @@ - + 4.0.0 - org.springframework.samples - spring-petclinic - 3.3.0-SNAPSHOT org.springframework.boot spring-boot-starter-parent - 3.3.3 + 3.4.0 + + + org.springframework.samples + spring-petclinic-ai + 3.4.0-SNAPSHOT + petclinic @@ -20,21 +21,23 @@ 17 UTF-8 UTF-8 - - 2023-05-10T07:42:50Z + + 2024-11-28T14:37:52Z + 1.0.1 5.3.3 4.7.0 14.1.2 - 10.16.0 + 10.20.1 0.8.12 0.2.29 1.0.0 - 3.3.1 + 3.6.0 0.0.11 - 0.0.41 + 0.0.43 1.0.0-M3 @@ -74,6 +77,11 @@ spring-boot-starter-test test + + + io.projectreactor + reactor-core + @@ -103,6 +111,11 @@ + + org.webjars + webjars-locator-lite + ${webjars-locator.version} + org.webjars.npm bootstrap @@ -178,8 +191,9 @@ - This build requires at least Java ${java.version}, update your JVM, and - run the build again + This build requires at least Java ${java.version}, + update your JVM, and + run the build again ${java.version} @@ -193,10 +207,10 @@ ${spring-format.version} - validate validate + validate @@ -219,19 +233,17 @@ nohttp-checkstyle-validation + + check + validate src/checkstyle/nohttp-checkstyle.xml ${basedir} **/* **/.git/**/*,**/.idea/**/*,**/target/**/,**/.flattened-pom.xml,**/*.class - - config_loc=${basedir}/src/checkstyle/ - + config_loc=${basedir}/src/checkstyle/ - - check - @@ -272,10 +284,10 @@ report - prepare-package report + prepare-package @@ -293,13 +305,13 @@ + org.cyclonedx cyclonedx-maven-plugin - Apache License, Version 2.0 @@ -309,39 +321,38 @@ - spring-snapshots - Spring Snapshots - https://repo.spring.io/snapshot true + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot - spring-milestones - Spring Milestones - https://repo.spring.io/milestone false + spring-milestones + Spring Milestones + https://repo.spring.io/milestone - - spring-snapshots - Spring Snapshots - https://repo.spring.io/snapshot true + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot - spring-milestones - Spring Milestones - https://repo.spring.io/milestone false + spring-milestones + Spring Milestones + https://repo.spring.io/milestone @@ -356,11 +367,11 @@ unpack - - generate-resources unpack + + generate-resources @@ -379,21 +390,20 @@ com.gitlab.haynes libsass-maven-plugin ${libsass.version} + + ${basedir}/src/main/scss/ + ${basedir}/src/main/resources/static/resources/css/ + ${project.build.directory}/webjars/META-INF/resources/webjars/bootstrap/${webjars-bootstrap.version}/scss/ + - generate-resources compile + generate-resources - - ${basedir}/src/main/scss/ - ${basedir}/src/main/resources/static/resources/css/ - - ${project.build.directory}/webjars/META-INF/resources/webjars/bootstrap/${webjars-bootstrap.version}/scss/ - @@ -427,7 +437,7 @@ - + @@ -440,7 +450,7 @@ - + @@ -453,7 +463,7 @@ - + @@ -465,5 +475,4 @@ - diff --git a/src/main/java/org/springframework/samples/petclinic/PetClinicRuntimeHints.java b/src/main/java/org/springframework/samples/petclinic/PetClinicRuntimeHints.java index 7acecf46..2975923d 100644 --- a/src/main/java/org/springframework/samples/petclinic/PetClinicRuntimeHints.java +++ b/src/main/java/org/springframework/samples/petclinic/PetClinicRuntimeHints.java @@ -28,7 +28,6 @@ public class PetClinicRuntimeHints implements RuntimeHintsRegistrar { public void registerHints(RuntimeHints hints, ClassLoader classLoader) { hints.resources().registerPattern("db/*"); // https://github.com/spring-projects/spring-boot/issues/32654 hints.resources().registerPattern("messages/*"); - hints.resources().registerPattern("META-INF/resources/webjars/*"); hints.resources().registerPattern("mysql-default-conf"); hints.serialization().registerType(BaseEntity.class); hints.serialization().registerType(Person.class); diff --git a/src/main/java/org/springframework/samples/petclinic/genai/AIDataProvider.java b/src/main/java/org/springframework/samples/petclinic/genai/AIDataProvider.java index 883ba7fe..90e88545 100644 --- a/src/main/java/org/springframework/samples/petclinic/genai/AIDataProvider.java +++ b/src/main/java/org/springframework/samples/petclinic/genai/AIDataProvider.java @@ -29,6 +29,7 @@ import org.springframework.stereotype.Service; import java.util.List; +import java.util.Optional; /** * Functions that are invoked by the LLM will use this bean to query the system of record @@ -70,10 +71,11 @@ public VetResponse getVets(VetRequest request) throws JsonProcessingException { } public AddedPetResponse addPetToOwner(AddPetRequest request) { - Owner owner = ownerRepository.findById(request.ownerId()); - owner.addPet(request.pet()); - this.ownerRepository.save(owner); - return new AddedPetResponse(owner); + var ownerWithPet = ownerRepository.findById(request.ownerId()).map(existingOwner -> { + existingOwner.addPet(request.pet()); + return ownerRepository.save(existingOwner); + }).orElse(null); + return new AddedPetResponse(ownerWithPet); } public OwnerResponse addOwnerToPetclinic(OwnerRequest ownerRequest) { diff --git a/src/main/java/org/springframework/samples/petclinic/genai/AIFunctionConfiguration.java b/src/main/java/org/springframework/samples/petclinic/genai/AIFunctionConfiguration.java index f4658a63..1e177f05 100644 --- a/src/main/java/org/springframework/samples/petclinic/genai/AIFunctionConfiguration.java +++ b/src/main/java/org/springframework/samples/petclinic/genai/AIFunctionConfiguration.java @@ -103,5 +103,4 @@ record VetResponse(List vet) { } record VetRequest(Vet vet) { - } diff --git a/src/main/java/org/springframework/samples/petclinic/model/NamedEntity.java b/src/main/java/org/springframework/samples/petclinic/model/NamedEntity.java index e91b9be3..004199ff 100644 --- a/src/main/java/org/springframework/samples/petclinic/model/NamedEntity.java +++ b/src/main/java/org/springframework/samples/petclinic/model/NamedEntity.java @@ -17,6 +17,7 @@ import jakarta.persistence.Column; import jakarta.persistence.MappedSuperclass; +import jakarta.validation.constraints.NotBlank; /** * Simple JavaBean domain object adds a name property to BaseEntity. Used as @@ -24,6 +25,7 @@ * * @author Ken Krebs * @author Juergen Hoeller + * @author Wick Dynex */ @MappedSuperclass public class NamedEntity extends BaseEntity { @@ -31,6 +33,7 @@ public class NamedEntity extends BaseEntity { private static final long serialVersionUID = -1827620691768236760L; @Column(name = "name") + @NotBlank private String name; public String getName() { diff --git a/src/main/java/org/springframework/samples/petclinic/owner/Owner.java b/src/main/java/org/springframework/samples/petclinic/owner/Owner.java index e7609332..f50cc1eb 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/Owner.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/Owner.java @@ -41,6 +41,7 @@ * @author Sam Brannen * @author Michael Isvy * @author Oliver Drotbohm + * @author Wick Dynex */ @Entity @Table(name = "owners") @@ -64,7 +65,7 @@ public class Owner extends Person { @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) @JoinColumn(name = "owner_id") @OrderBy("name") - private List pets = new ArrayList<>(); + private final List pets = new ArrayList<>(); public String getAddress() { return this.address; @@ -103,7 +104,7 @@ public void addPet(Pet pet) { /** * Return the Pet with the given name, or null if none found for this Owner. * @param name to test - * @return a pet if pet name is already in use + * @return the Pet with the given name, or null if no such Pet exists for this Owner */ public Pet getPet(String name) { return getPet(name, false); @@ -112,7 +113,7 @@ public Pet getPet(String name) { /** * Return the Pet with the given id, or null if none found for this Owner. * @param id to test - * @return a pet if pet id is already in use + * @return the Pet with the given id, or null if no such Pet exists for this Owner */ public Pet getPet(Integer id) { for (Pet pet : getPets()) { @@ -129,10 +130,10 @@ public Pet getPet(Integer id) { /** * Return the Pet with the given name, or null if none found for this Owner. * @param name to test - * @return a pet if pet name is already in use + * @param ignoreNew whether to ignore new pets (pets that are not saved yet) + * @return the Pet with the given name, or null if no such Pet exists for this Owner */ public Pet getPet(String name, boolean ignoreNew) { - name = name.toLowerCase(); for (Pet pet : getPets()) { String compName = pet.getName(); if (compName != null && compName.equalsIgnoreCase(name)) { diff --git a/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java b/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java index 46439161..71e8712f 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java @@ -16,8 +16,7 @@ package org.springframework.samples.petclinic.owner; import java.util.List; -import java.util.Map; - +import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -41,6 +40,7 @@ * @author Ken Krebs * @author Arjen Poutsma * @author Michael Isvy + * @author Wick Dynex */ @Controller class OwnerController { @@ -49,8 +49,8 @@ class OwnerController { private final OwnerRepository owners; - public OwnerController(OwnerRepository clinicService) { - this.owners = clinicService; + public OwnerController(OwnerRepository owners) { + this.owners = owners; } @InitBinder @@ -60,13 +60,14 @@ public void setAllowedFields(WebDataBinder dataBinder) { @ModelAttribute("owner") public Owner findOwner(@PathVariable(name = "ownerId", required = false) Integer ownerId) { - return ownerId == null ? new Owner() : this.owners.findById(ownerId); + return ownerId == null ? new Owner() + : this.owners.findById(ownerId) + .orElseThrow(() -> new IllegalArgumentException("Owner not found with id: " + ownerId + + ". Please ensure the ID is correct " + "and the owner exists in the database.")); } @GetMapping("/owners/new") - public String initCreationForm(Map model) { - Owner owner = new Owner(); - model.put("owner", owner); + public String initCreationForm() { return VIEWS_OWNER_CREATE_OR_UPDATE_FORM; } @@ -125,13 +126,11 @@ private String addPaginationModel(int page, Model model, Page paginated) private Page findPaginatedForOwnersLastName(int page, String lastname) { int pageSize = 5; Pageable pageable = PageRequest.of(page - 1, pageSize); - return owners.findByLastName(lastname, pageable); + return owners.findByLastNameStartingWith(lastname, pageable); } @GetMapping("/owners/{ownerId}/edit") - public String initUpdateOwnerForm(@PathVariable("ownerId") int ownerId, Model model) { - Owner owner = this.owners.findById(ownerId); - model.addAttribute(owner); + public String initUpdateOwnerForm() { return VIEWS_OWNER_CREATE_OR_UPDATE_FORM; } @@ -143,6 +142,12 @@ public String processUpdateOwnerForm(@Valid Owner owner, BindingResult result, @ return VIEWS_OWNER_CREATE_OR_UPDATE_FORM; } + if (owner.getId() != ownerId) { + result.rejectValue("id", "mismatch", "The owner ID in the form does not match the URL."); + redirectAttributes.addFlashAttribute("error", "Owner ID mismatch. Please try again."); + return "redirect:/owners/{ownerId}/edit"; + } + owner.setId(ownerId); this.owners.save(owner); redirectAttributes.addFlashAttribute("message", "Owner Values Updated"); @@ -157,7 +162,9 @@ public String processUpdateOwnerForm(@Valid Owner owner, BindingResult result, @ @GetMapping("/owners/{ownerId}") public ModelAndView showOwner(@PathVariable("ownerId") int ownerId) { ModelAndView mav = new ModelAndView("owners/ownerDetails"); - Owner owner = this.owners.findById(ownerId); + Optional optionalOwner = this.owners.findById(ownerId); + Owner owner = optionalOwner.orElseThrow(() -> new IllegalArgumentException( + "Owner not found with id: " + ownerId + ". Please ensure the ID is correct ")); mav.addObject(owner); return mav; } diff --git a/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java b/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java index f4444943..5d7a40fb 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java @@ -16,12 +16,13 @@ package org.springframework.samples.petclinic.owner; import java.util.List; +import java.util.Optional; +import jakarta.annotation.Nonnull; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.Repository; -import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; /** @@ -34,15 +35,15 @@ * @author Juergen Hoeller * @author Sam Brannen * @author Michael Isvy + * @author Wick Dynex */ -public interface OwnerRepository extends Repository { +public interface OwnerRepository extends JpaRepository { /** * Retrieve all {@link PetType}s from the data store. * @return a Collection of {@link PetType}s. */ @Query("SELECT ptype FROM PetType ptype ORDER BY ptype.name") - @Transactional(readOnly = true) List findPetTypes(); /** @@ -52,31 +53,26 @@ public interface OwnerRepository extends Repository { * @return a Collection of matching {@link Owner}s (or an empty Collection if none * found) */ - - @Query("SELECT DISTINCT owner FROM Owner owner left join owner.pets WHERE owner.lastName LIKE :lastName% ") - @Transactional(readOnly = true) - Page findByLastName(@Param("lastName") String lastName, Pageable pageable); + Page findByLastNameStartingWith(String lastName, Pageable pageable); /** * Retrieve an {@link Owner} from the data store by id. + *

+ * This method returns an {@link Optional} containing the {@link Owner} if found. If + * no {@link Owner} is found with the provided id, it will return an empty + * {@link Optional}. + *

* @param id the id to search for - * @return the {@link Owner} if found - */ - @Query("SELECT owner FROM Owner owner left join fetch owner.pets WHERE owner.id =:id") - @Transactional(readOnly = true) - Owner findById(@Param("id") Integer id); - - /** - * Save an {@link Owner} to the data store, either inserting or updating it. - * @param owner the {@link Owner} to save + * @return an {@link Optional} containing the {@link Owner} if found, or an empty + * {@link Optional} if not found. + * @throws IllegalArgumentException if the id is null (assuming null is not a valid + * input for id) */ - void save(Owner owner); + Optional findById(@Nonnull Integer id); /** * Returns all the owners from data store **/ - @Query("SELECT owner FROM Owner owner") - @Transactional(readOnly = true) Page findAll(Pageable pageable); } diff --git a/src/main/java/org/springframework/samples/petclinic/owner/Pet.java b/src/main/java/org/springframework/samples/petclinic/owner/Pet.java index 58f21bef..138f2b51 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/Pet.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/Pet.java @@ -39,6 +39,7 @@ * @author Ken Krebs * @author Juergen Hoeller * @author Sam Brannen + * @author Wick Dynex */ @Entity @Table(name = "pets") @@ -57,7 +58,7 @@ public class Pet extends NamedEntity { @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) @JoinColumn(name = "pet_id") @OrderBy("visit_date ASC") - private Set visits = new LinkedHashSet<>(); + private final Set visits = new LinkedHashSet<>(); public void setBirthDate(LocalDate birthDate) { this.birthDate = birthDate; diff --git a/src/main/java/org/springframework/samples/petclinic/owner/PetController.java b/src/main/java/org/springframework/samples/petclinic/owner/PetController.java index 781fb580..fcf431bf 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/PetController.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/PetController.java @@ -17,6 +17,7 @@ import java.time.LocalDate; import java.util.Collection; +import java.util.Optional; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; @@ -37,6 +38,7 @@ * @author Juergen Hoeller * @author Ken Krebs * @author Arjen Poutsma + * @author Wick Dynex */ @Controller @RequestMapping("/owners/{ownerId}") @@ -57,11 +59,9 @@ public Collection populatePetTypes() { @ModelAttribute("owner") public Owner findOwner(@PathVariable("ownerId") int ownerId) { - - Owner owner = this.owners.findById(ownerId); - if (owner == null) { - throw new IllegalArgumentException("Owner ID not found: " + ownerId); - } + Optional optionalOwner = this.owners.findById(ownerId); + Owner owner = optionalOwner.orElseThrow(() -> new IllegalArgumentException( + "Owner not found with id: " + ownerId + ". Please ensure the ID is correct ")); return owner; } @@ -73,10 +73,9 @@ public Pet findPet(@PathVariable("ownerId") int ownerId, return new Pet(); } - Owner owner = this.owners.findById(ownerId); - if (owner == null) { - throw new IllegalArgumentException("Owner ID not found: " + ownerId); - } + Optional optionalOwner = this.owners.findById(ownerId); + Owner owner = optionalOwner.orElseThrow(() -> new IllegalArgumentException( + "Owner not found with id: " + ownerId + ". Please ensure the ID is correct ")); return owner.getPet(petId); } @@ -94,51 +93,46 @@ public void initPetBinder(WebDataBinder dataBinder) { public String initCreationForm(Owner owner, ModelMap model) { Pet pet = new Pet(); owner.addPet(pet); - model.put("pet", pet); return VIEWS_PETS_CREATE_OR_UPDATE_FORM; } @PostMapping("/pets/new") - public String processCreationForm(Owner owner, @Valid Pet pet, BindingResult result, ModelMap model, + public String processCreationForm(Owner owner, @Valid Pet pet, BindingResult result, RedirectAttributes redirectAttributes) { - if (StringUtils.hasText(pet.getName()) && pet.isNew() && owner.getPet(pet.getName(), true) != null) { + + if (StringUtils.hasText(pet.getName()) && pet.isNew() && owner.getPet(pet.getName(), true) != null) result.rejectValue("name", "duplicate", "already exists"); - } LocalDate currentDate = LocalDate.now(); if (pet.getBirthDate() != null && pet.getBirthDate().isAfter(currentDate)) { result.rejectValue("birthDate", "typeMismatch.birthDate"); } - owner.addPet(pet); if (result.hasErrors()) { - model.put("pet", pet); return VIEWS_PETS_CREATE_OR_UPDATE_FORM; } + owner.addPet(pet); this.owners.save(owner); redirectAttributes.addFlashAttribute("message", "New Pet has been Added"); return "redirect:/owners/{ownerId}"; } @GetMapping("/pets/{petId}/edit") - public String initUpdateForm(Owner owner, @PathVariable("petId") int petId, ModelMap model, - RedirectAttributes redirectAttributes) { - Pet pet = owner.getPet(petId); - model.put("pet", pet); + public String initUpdateForm() { return VIEWS_PETS_CREATE_OR_UPDATE_FORM; } @PostMapping("/pets/{petId}/edit") - public String processUpdateForm(@Valid Pet pet, BindingResult result, Owner owner, ModelMap model, + public String processUpdateForm(Owner owner, @Valid Pet pet, BindingResult result, RedirectAttributes redirectAttributes) { String petName = pet.getName(); // checking if the pet name already exist for the owner if (StringUtils.hasText(petName)) { - Pet existingPet = owner.getPet(petName.toLowerCase(), false); - if (existingPet != null && existingPet.getId() != pet.getId()) { + Pet existingPet = owner.getPet(petName, false); + if (existingPet != null && !existingPet.getId().equals(pet.getId())) { result.rejectValue("name", "duplicate", "already exists"); } } @@ -149,7 +143,6 @@ public String processUpdateForm(@Valid Pet pet, BindingResult result, Owner owne } if (result.hasErrors()) { - model.put("pet", pet); return VIEWS_PETS_CREATE_OR_UPDATE_FORM; } diff --git a/src/main/java/org/springframework/samples/petclinic/owner/VisitController.java b/src/main/java/org/springframework/samples/petclinic/owner/VisitController.java index 6c7a8dd1..b546f609 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/VisitController.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/VisitController.java @@ -16,6 +16,7 @@ package org.springframework.samples.petclinic.owner; import java.util.Map; +import java.util.Optional; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; @@ -35,6 +36,7 @@ * @author Arjen Poutsma * @author Michael Isvy * @author Dave Syer + * @author Wick Dynex */ @Controller class VisitController { @@ -60,7 +62,9 @@ public void setAllowedFields(WebDataBinder dataBinder) { @ModelAttribute("visit") public Visit loadPetWithVisit(@PathVariable("ownerId") int ownerId, @PathVariable("petId") int petId, Map model) { - Owner owner = this.owners.findById(ownerId); + Optional optionalOwner = owners.findById(ownerId); + Owner owner = optionalOwner.orElseThrow(() -> new IllegalArgumentException( + "Owner not found with id: " + ownerId + ". Please ensure the ID is correct ")); Pet pet = owner.getPet(petId); model.put("pet", pet); diff --git a/src/main/java/org/springframework/samples/petclinic/vet/Vet.java b/src/main/java/org/springframework/samples/petclinic/vet/Vet.java index 080b6585..8f3dc075 100644 --- a/src/main/java/org/springframework/samples/petclinic/vet/Vet.java +++ b/src/main/java/org/springframework/samples/petclinic/vet/Vet.java @@ -62,10 +62,6 @@ protected Set getSpecialtiesInternal() { return this.specialties; } - protected void setSpecialtiesInternal(Set specialties) { - this.specialties = specialties; - } - @JsonProperty("specialties") @JsonSerialize(as = ArrayList.class) public List getSpecialties() { @@ -84,4 +80,4 @@ public void addSpecialty(Specialty specialty) { getSpecialtiesInternal().add(specialty); } -} \ No newline at end of file +} diff --git a/src/main/java/org/springframework/samples/petclinic/vet/VetController.java b/src/main/java/org/springframework/samples/petclinic/vet/VetController.java index c530c7a8..1e03b0f9 100644 --- a/src/main/java/org/springframework/samples/petclinic/vet/VetController.java +++ b/src/main/java/org/springframework/samples/petclinic/vet/VetController.java @@ -37,8 +37,8 @@ class VetController { private final VetRepository vetRepository; - public VetController(VetRepository clinicService) { - this.vetRepository = clinicService; + public VetController(VetRepository vetRepository) { + this.vetRepository = vetRepository; } @GetMapping("/vets.html") diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 27c4b20a..8922bea8 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,7 +9,7 @@ spring.thymeleaf.mode=HTML # JPA spring.jpa.hibernate.ddl-auto=none -spring.jpa.open-in-view=true +spring.jpa.open-in-view=false # Internationalization spring.messages.basename=messages/messages diff --git a/src/main/resources/messages/messages_fa.properties b/src/main/resources/messages/messages_fa.properties new file mode 100644 index 00000000..a68a21c7 --- /dev/null +++ b/src/main/resources/messages/messages_fa.properties @@ -0,0 +1,9 @@ +welcome=خوش آمدید +required=الزامی +notFound=یافت نشد +duplicate=قبلا استفاده شده +nonNumeric=باید عددی باشد +duplicateFormSubmission=ارسال تکراری فرم مجاز نیست +typeMismatch.date=تاریخ نامعتبر +typeMismatch.birthDate=تاریخ تولد نامعتبر + diff --git a/src/main/resources/messages/messages_pt.properties b/src/main/resources/messages/messages_pt.properties new file mode 100644 index 00000000..e9bc35a3 --- /dev/null +++ b/src/main/resources/messages/messages_pt.properties @@ -0,0 +1,8 @@ +welcome=Bem-vindo +required=E necessario +notFound=Nao foi encontrado +duplicate=Ja esta em uso +nonNumeric=Deve ser tudo numerico +duplicateFormSubmission=O envio duplicado de formulario nao e permitido +typeMismatch.date=Data invalida +typeMismatch.birthDate=Data de nascimento invalida diff --git a/src/main/resources/messages/messages_ru.properties b/src/main/resources/messages/messages_ru.properties new file mode 100644 index 00000000..7e8d54d2 --- /dev/null +++ b/src/main/resources/messages/messages_ru.properties @@ -0,0 +1,9 @@ +welcome=Добро пожаловать +required=необходимо +notFound=не найдено +duplicate=уже используется +nonNumeric=должно быть все числовое значение +duplicateFormSubmission=Дублирование формы не допускается +typeMismatch.date=неправильная даные +typeMismatch.birthDate=неправильная дата + diff --git a/src/main/resources/messages/messages_tr.properties b/src/main/resources/messages/messages_tr.properties new file mode 100644 index 00000000..1020566a --- /dev/null +++ b/src/main/resources/messages/messages_tr.properties @@ -0,0 +1,9 @@ +welcome=hoş geldiniz +required=gerekli +notFound=bulunamadı +duplicate=zaten kullanılıyor +nonNumeric=sadece sayısal olmalıdır +duplicateFormSubmission=Formun tekrar gönderilmesine izin verilmez +typeMismatch.date=geçersiz tarih +typeMismatch.birthDate=geçersiz tarih + diff --git a/src/main/resources/templates/fragments/layout.html b/src/main/resources/templates/fragments/layout.html index 420b8dc4..395cb024 100644 --- a/src/main/resources/templates/fragments/layout.html +++ b/src/main/resources/templates/fragments/layout.html @@ -17,7 +17,7 @@ - + @@ -104,7 +104,7 @@ - + diff --git a/src/test/java/org/springframework/samples/petclinic/MySqlIntegrationTests.java b/src/test/java/org/springframework/samples/petclinic/MySqlIntegrationTests.java index 08d0d712..d67e449d 100644 --- a/src/test/java/org/springframework/samples/petclinic/MySqlIntegrationTests.java +++ b/src/test/java/org/springframework/samples/petclinic/MySqlIntegrationTests.java @@ -46,7 +46,7 @@ class MySqlIntegrationTests { @ServiceConnection @Container - static MySQLContainer container = new MySQLContainer<>("mysql:8.4"); + static MySQLContainer container = new MySQLContainer<>("mysql:9.1"); @LocalServerPort int port; @@ -58,7 +58,7 @@ class MySqlIntegrationTests { private RestTemplateBuilder builder; @Test - void testFindAll() throws Exception { + void testFindAll() { vets.findAll(); vets.findAll(); // served from cache } diff --git a/src/test/java/org/springframework/samples/petclinic/MysqlTestApplication.java b/src/test/java/org/springframework/samples/petclinic/MysqlTestApplication.java index 3c582d67..8c7560a1 100644 --- a/src/test/java/org/springframework/samples/petclinic/MysqlTestApplication.java +++ b/src/test/java/org/springframework/samples/petclinic/MysqlTestApplication.java @@ -36,7 +36,7 @@ public class MysqlTestApplication { @Profile("mysql") @Bean static MySQLContainer container() { - return new MySQLContainer<>("mysql:8.4"); + return new MySQLContainer<>("mysql:9.1"); } public static void main(String[] args) { diff --git a/src/test/java/org/springframework/samples/petclinic/PetClinicIntegrationTests.java b/src/test/java/org/springframework/samples/petclinic/PetClinicIntegrationTests.java index 6472a121..6d982069 100644 --- a/src/test/java/org/springframework/samples/petclinic/PetClinicIntegrationTests.java +++ b/src/test/java/org/springframework/samples/petclinic/PetClinicIntegrationTests.java @@ -44,7 +44,7 @@ public class PetClinicIntegrationTests { private RestTemplateBuilder builder; @Test - void testFindAll() throws Exception { + void testFindAll() { vets.findAll(); vets.findAll(); // served from cache } diff --git a/src/test/java/org/springframework/samples/petclinic/PostgresIntegrationTests.java b/src/test/java/org/springframework/samples/petclinic/PostgresIntegrationTests.java index 18945a57..0b9e4f93 100644 --- a/src/test/java/org/springframework/samples/petclinic/PostgresIntegrationTests.java +++ b/src/test/java/org/springframework/samples/petclinic/PostgresIntegrationTests.java @@ -17,6 +17,7 @@ package org.springframework.samples.petclinic; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assumptions.assumeTrue; import java.util.Arrays; @@ -48,7 +49,7 @@ import org.testcontainers.DockerClientFactory; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "spring.docker.compose.skip.in-tests=false", // - "spring.docker.compose.profiles.active=postgres" }) + "spring.docker.compose.start.arguments=--force-recreate,--renew-anon-volumes,postgres" }) @ActiveProfiles("postgres") @DisabledInNativeImage public class PostgresIntegrationTests { @@ -71,7 +72,7 @@ public static void main(String[] args) { new SpringApplicationBuilder(PetClinicApplication.class) // .profiles("postgres") // .properties( // - "spring.docker.compose.profiles.active=postgres" // + "spring.docker.compose.start.arguments=postgres" // ) // .listeners(new PropertiesLogger()) // .run(args); @@ -114,7 +115,16 @@ public void printProperties() { Arrays.sort(names); for (String name : names) { String resolved = environment.getProperty(name); - String value = source.getProperty(name).toString(); + + assertNotNull(resolved, "resolved environment property: " + name + " is null."); + + Object sourceProperty = source.getProperty(name); + + assertNotNull(sourceProperty, "source property was expecting an object but is null."); + + assertNotNull(sourceProperty.toString(), "source property toString() returned null."); + + String value = sourceProperty.toString(); if (resolved.equals(value)) { log.info(name + "=" + resolved); } diff --git a/src/test/java/org/springframework/samples/petclinic/owner/OwnerControllerTests.java b/src/test/java/org/springframework/samples/petclinic/owner/OwnerControllerTests.java index fc22fb01..426ca5c2 100644 --- a/src/test/java/org/springframework/samples/petclinic/owner/OwnerControllerTests.java +++ b/src/test/java/org/springframework/samples/petclinic/owner/OwnerControllerTests.java @@ -20,17 +20,18 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledInNativeImage; -import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import java.time.LocalDate; +import java.util.Optional; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.greaterThan; @@ -43,16 +44,16 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; /** * Test class for {@link OwnerController} * * @author Colin But + * @author Wick Dynex */ @WebMvcTest(OwnerController.class) @DisabledInNativeImage @@ -64,7 +65,7 @@ class OwnerControllerTests { @Autowired private MockMvc mockMvc; - @MockBean + @MockitoBean private OwnerRepository owners; private Owner george() { @@ -90,12 +91,12 @@ private Owner george() { void setup() { Owner george = george(); - given(this.owners.findByLastName(eq("Franklin"), any(Pageable.class))) + given(this.owners.findByLastNameStartingWith(eq("Franklin"), any(Pageable.class))) .willReturn(new PageImpl<>(Lists.newArrayList(george))); given(this.owners.findAll(any(Pageable.class))).willReturn(new PageImpl<>(Lists.newArrayList(george))); - given(this.owners.findById(TEST_OWNER_ID)).willReturn(george); + given(this.owners.findById(TEST_OWNER_ID)).willReturn(Optional.of(george)); Visit visit = new Visit(); visit.setDate(LocalDate.now()); george.getPet("Max").getVisits().add(visit); @@ -143,14 +144,14 @@ void testInitFindForm() throws Exception { @Test void testProcessFindFormSuccess() throws Exception { Page tasks = new PageImpl<>(Lists.newArrayList(george(), new Owner())); - Mockito.when(this.owners.findByLastName(anyString(), any(Pageable.class))).thenReturn(tasks); + when(this.owners.findByLastNameStartingWith(anyString(), any(Pageable.class))).thenReturn(tasks); mockMvc.perform(get("/owners?page=1")).andExpect(status().isOk()).andExpect(view().name("owners/ownersList")); } @Test void testProcessFindFormByLastName() throws Exception { Page tasks = new PageImpl<>(Lists.newArrayList(george())); - Mockito.when(this.owners.findByLastName(eq("Franklin"), any(Pageable.class))).thenReturn(tasks); + when(this.owners.findByLastNameStartingWith(eq("Franklin"), any(Pageable.class))).thenReturn(tasks); mockMvc.perform(get("/owners?page=1").param("lastName", "Franklin")) .andExpect(status().is3xxRedirection()) .andExpect(view().name("redirect:/owners/" + TEST_OWNER_ID)); @@ -159,7 +160,7 @@ void testProcessFindFormByLastName() throws Exception { @Test void testProcessFindFormNoOwnersFound() throws Exception { Page tasks = new PageImpl<>(Lists.newArrayList()); - Mockito.when(this.owners.findByLastName(eq("Unknown Surname"), any(Pageable.class))).thenReturn(tasks); + when(this.owners.findByLastNameStartingWith(eq("Unknown Surname"), any(Pageable.class))).thenReturn(tasks); mockMvc.perform(get("/owners?page=1").param("lastName", "Unknown Surname")) .andExpect(status().isOk()) .andExpect(model().attributeHasFieldErrors("owner", "lastName")) @@ -229,4 +230,24 @@ void testShowOwner() throws Exception { .andExpect(view().name("owners/ownerDetails")); } + @Test + public void testProcessUpdateOwnerFormWithIdMismatch() throws Exception { + int pathOwnerId = 1; + + Owner owner = new Owner(); + owner.setId(2); + owner.setFirstName("John"); + owner.setLastName("Doe"); + owner.setAddress("Center Street"); + owner.setCity("New York"); + owner.setTelephone("0123456789"); + + when(owners.findById(pathOwnerId)).thenReturn(Optional.of(owner)); + + mockMvc.perform(MockMvcRequestBuilders.post("/owners/{ownerId}/edit", pathOwnerId).flashAttr("owner", owner)) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/owners/" + pathOwnerId + "/edit")) + .andExpect(flash().attributeExists("error")); + } + } diff --git a/src/test/java/org/springframework/samples/petclinic/owner/PetControllerTests.java b/src/test/java/org/springframework/samples/petclinic/owner/PetControllerTests.java index 73b83f9f..9a6134cb 100644 --- a/src/test/java/org/springframework/samples/petclinic/owner/PetControllerTests.java +++ b/src/test/java/org/springframework/samples/petclinic/owner/PetControllerTests.java @@ -18,16 +18,20 @@ import org.assertj.core.util.Lists; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledInNativeImage; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.FilterType; import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; +import java.time.LocalDate; +import java.util.Optional; + import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -39,6 +43,7 @@ * Test class for the {@link PetController} * * @author Colin But + * @author Wick Dynex */ @WebMvcTest(value = PetController.class, includeFilters = @ComponentScan.Filter(value = PetTypeFormatter.class, type = FilterType.ASSIGNABLE_TYPE)) @@ -53,7 +58,7 @@ class PetControllerTests { @Autowired private MockMvc mockMvc; - @MockBean + @MockitoBean private OwnerRepository owners; @BeforeEach @@ -62,11 +67,17 @@ void setup() { cat.setId(3); cat.setName("hamster"); given(this.owners.findPetTypes()).willReturn(Lists.newArrayList(cat)); + Owner owner = new Owner(); Pet pet = new Pet(); + Pet dog = new Pet(); owner.addPet(pet); + owner.addPet(dog); pet.setId(TEST_PET_ID); - given(this.owners.findById(TEST_OWNER_ID)).willReturn(owner); + dog.setId(TEST_PET_ID + 1); + pet.setName("petty"); + dog.setName("doggy"); + given(this.owners.findById(TEST_OWNER_ID)).willReturn(Optional.of(owner)); } @Test @@ -87,25 +98,72 @@ void testProcessCreationFormSuccess() throws Exception { .andExpect(view().name("redirect:/owners/{ownerId}")); } - @Test - void testProcessCreationFormHasErrors() throws Exception { - mockMvc - .perform(post("/owners/{ownerId}/pets/new", TEST_OWNER_ID).param("name", "Betty") - .param("birthDate", "2015-02-12")) - .andExpect(model().attributeHasNoErrors("owner")) - .andExpect(model().attributeHasErrors("pet")) - .andExpect(model().attributeHasFieldErrors("pet", "type")) - .andExpect(model().attributeHasFieldErrorCode("pet", "type", "required")) - .andExpect(status().isOk()) - .andExpect(view().name("pets/createOrUpdatePetForm")); - } + @Nested + class ProcessCreationFormHasErrors { + + @Test + void testProcessCreationFormWithBlankName() throws Exception { + mockMvc + .perform(post("/owners/{ownerId}/pets/new", TEST_OWNER_ID).param("name", "\t \n") + .param("birthDate", "2015-02-12")) + .andExpect(model().attributeHasNoErrors("owner")) + .andExpect(model().attributeHasErrors("pet")) + .andExpect(model().attributeHasFieldErrors("pet", "name")) + .andExpect(model().attributeHasFieldErrorCode("pet", "name", "required")) + .andExpect(status().isOk()) + .andExpect(view().name("pets/createOrUpdatePetForm")); + } + + @Test + void testProcessCreationFormWithDuplicateName() throws Exception { + mockMvc + .perform(post("/owners/{ownerId}/pets/new", TEST_OWNER_ID).param("name", "petty") + .param("birthDate", "2015-02-12")) + .andExpect(model().attributeHasNoErrors("owner")) + .andExpect(model().attributeHasErrors("pet")) + .andExpect(model().attributeHasFieldErrors("pet", "name")) + .andExpect(model().attributeHasFieldErrorCode("pet", "name", "duplicate")) + .andExpect(status().isOk()) + .andExpect(view().name("pets/createOrUpdatePetForm")); + } + + @Test + void testProcessCreationFormWithMissingPetType() throws Exception { + mockMvc + .perform(post("/owners/{ownerId}/pets/new", TEST_OWNER_ID).param("name", "Betty") + .param("birthDate", "2015-02-12")) + .andExpect(model().attributeHasNoErrors("owner")) + .andExpect(model().attributeHasErrors("pet")) + .andExpect(model().attributeHasFieldErrors("pet", "type")) + .andExpect(model().attributeHasFieldErrorCode("pet", "type", "required")) + .andExpect(status().isOk()) + .andExpect(view().name("pets/createOrUpdatePetForm")); + } + + @Test + void testProcessCreationFormWithInvalidBirthDate() throws Exception { + LocalDate currentDate = LocalDate.now(); + String futureBirthDate = currentDate.plusMonths(1).toString(); + + mockMvc + .perform(post("/owners/{ownerId}/pets/new", TEST_OWNER_ID).param("name", "Betty") + .param("birthDate", futureBirthDate)) + .andExpect(model().attributeHasNoErrors("owner")) + .andExpect(model().attributeHasErrors("pet")) + .andExpect(model().attributeHasFieldErrors("pet", "birthDate")) + .andExpect(model().attributeHasFieldErrorCode("pet", "birthDate", "typeMismatch.birthDate")) + .andExpect(status().isOk()) + .andExpect(view().name("pets/createOrUpdatePetForm")); + } + + @Test + void testInitUpdateForm() throws Exception { + mockMvc.perform(get("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID)) + .andExpect(status().isOk()) + .andExpect(model().attributeExists("pet")) + .andExpect(view().name("pets/createOrUpdatePetForm")); + } - @Test - void testInitUpdateForm() throws Exception { - mockMvc.perform(get("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID)) - .andExpect(status().isOk()) - .andExpect(model().attributeExists("pet")) - .andExpect(view().name("pets/createOrUpdatePetForm")); } @Test @@ -118,15 +176,33 @@ void testProcessUpdateFormSuccess() throws Exception { .andExpect(view().name("redirect:/owners/{ownerId}")); } - @Test - void testProcessUpdateFormHasErrors() throws Exception { - mockMvc - .perform(post("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID).param("name", "Betty") - .param("birthDate", "2015/02/12")) - .andExpect(model().attributeHasNoErrors("owner")) - .andExpect(model().attributeHasErrors("pet")) - .andExpect(status().isOk()) - .andExpect(view().name("pets/createOrUpdatePetForm")); + @Nested + class ProcessUpdateFormHasErrors { + + @Test + void testProcessUpdateFormWithInvalidBirthDate() throws Exception { + mockMvc + .perform(post("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID).param("name", " ") + .param("birthDate", "2015/02/12")) + .andExpect(model().attributeHasNoErrors("owner")) + .andExpect(model().attributeHasErrors("pet")) + .andExpect(model().attributeHasFieldErrors("pet", "birthDate")) + .andExpect(model().attributeHasFieldErrorCode("pet", "birthDate", "typeMismatch")) + .andExpect(view().name("pets/createOrUpdatePetForm")); + } + + @Test + void testProcessUpdateFormWithBlankName() throws Exception { + mockMvc + .perform(post("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID).param("name", " ") + .param("birthDate", "2015-02-12")) + .andExpect(model().attributeHasNoErrors("owner")) + .andExpect(model().attributeHasErrors("pet")) + .andExpect(model().attributeHasFieldErrors("pet", "name")) + .andExpect(model().attributeHasFieldErrorCode("pet", "name", "required")) + .andExpect(view().name("pets/createOrUpdatePetForm")); + } + } } diff --git a/src/test/java/org/springframework/samples/petclinic/owner/PetTypeFormatterTests.java b/src/test/java/org/springframework/samples/petclinic/owner/PetTypeFormatterTests.java index b51b60e9..337ef42e 100644 --- a/src/test/java/org/springframework/samples/petclinic/owner/PetTypeFormatterTests.java +++ b/src/test/java/org/springframework/samples/petclinic/owner/PetTypeFormatterTests.java @@ -68,7 +68,7 @@ void shouldParse() throws ParseException { } @Test - void shouldThrowParseException() throws ParseException { + void shouldThrowParseException() { given(this.pets.findPetTypes()).willReturn(makePetTypes()); Assertions.assertThrows(ParseException.class, () -> { petTypeFormatter.parse("Fish", Locale.ENGLISH); diff --git a/src/test/java/org/springframework/samples/petclinic/owner/PetValidatorTests.java b/src/test/java/org/springframework/samples/petclinic/owner/PetValidatorTests.java new file mode 100644 index 00000000..1a153bcb --- /dev/null +++ b/src/test/java/org/springframework/samples/petclinic/owner/PetValidatorTests.java @@ -0,0 +1,117 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.samples.petclinic.owner; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledInNativeImage; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.validation.Errors; +import org.springframework.validation.MapBindingResult; + +import java.time.LocalDate; +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test class for {@link PetValidator} + * + * @author Wick Dynex + */ +@ExtendWith(MockitoExtension.class) +@DisabledInNativeImage +public class PetValidatorTests { + + private PetValidator petValidator; + + private Pet pet; + + private PetType petType; + + private Errors errors; + + private static final String petName = "Buddy"; + + private static final String petTypeName = "Dog"; + + private static final LocalDate petBirthDate = LocalDate.of(1990, 1, 1); + + @BeforeEach + void setUp() { + petValidator = new PetValidator(); + pet = new Pet(); + petType = new PetType(); + errors = new MapBindingResult(new HashMap<>(), "pet"); + } + + @Test + void testValidate() { + petType.setName(petTypeName); + pet.setName(petName); + pet.setType(petType); + pet.setBirthDate(petBirthDate); + + petValidator.validate(pet, errors); + + assertFalse(errors.hasErrors()); + } + + @Nested + class ValidateHasErrors { + + @Test + void testValidateWithInvalidPetName() { + petType.setName(petTypeName); + pet.setName(""); + pet.setType(petType); + pet.setBirthDate(petBirthDate); + + petValidator.validate(pet, errors); + + assertTrue(errors.hasFieldErrors("name")); + } + + @Test + void testValidateWithInvalidPetType() { + pet.setName(petName); + pet.setType(null); + pet.setBirthDate(petBirthDate); + + petValidator.validate(pet, errors); + + assertTrue(errors.hasFieldErrors("type")); + } + + @Test + void testValidateWithInvalidBirthDate() { + petType.setName(petTypeName); + pet.setName(petName); + pet.setType(petType); + pet.setBirthDate(null); + + petValidator.validate(pet, errors); + + assertTrue(errors.hasFieldErrors("birthDate")); + } + + } + +} diff --git a/src/test/java/org/springframework/samples/petclinic/owner/VisitControllerTests.java b/src/test/java/org/springframework/samples/petclinic/owner/VisitControllerTests.java index 3565d927..e42e7503 100644 --- a/src/test/java/org/springframework/samples/petclinic/owner/VisitControllerTests.java +++ b/src/test/java/org/springframework/samples/petclinic/owner/VisitControllerTests.java @@ -28,14 +28,17 @@ import org.junit.jupiter.api.condition.DisabledInNativeImage; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; +import java.util.Optional; + /** * Test class for {@link VisitController} * * @author Colin But + * @author Wick Dynex */ @WebMvcTest(VisitController.class) @DisabledInNativeImage @@ -49,7 +52,7 @@ class VisitControllerTests { @Autowired private MockMvc mockMvc; - @MockBean + @MockitoBean private OwnerRepository owners; @BeforeEach @@ -58,7 +61,7 @@ void init() { Pet pet = new Pet(); owner.addPet(pet); pet.setId(TEST_PET_ID); - given(this.owners.findById(TEST_OWNER_ID)).willReturn(owner); + given(this.owners.findById(TEST_OWNER_ID)).willReturn(Optional.of(owner)); } @Test diff --git a/src/test/java/org/springframework/samples/petclinic/service/ClinicServiceTests.java b/src/test/java/org/springframework/samples/petclinic/service/ClinicServiceTests.java index d15fcea5..12f43a86 100644 --- a/src/test/java/org/springframework/samples/petclinic/service/ClinicServiceTests.java +++ b/src/test/java/org/springframework/samples/petclinic/service/ClinicServiceTests.java @@ -20,13 +20,13 @@ import java.time.LocalDate; import java.util.Collection; +import java.util.Optional; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.context.annotation.ComponentScan; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.samples.petclinic.owner.Owner; @@ -36,7 +36,6 @@ import org.springframework.samples.petclinic.owner.Visit; import org.springframework.samples.petclinic.vet.Vet; import org.springframework.samples.petclinic.vet.VetRepository; -import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /** @@ -83,16 +82,18 @@ class ClinicServiceTests { @Test void shouldFindOwnersByLastName() { - Page owners = this.owners.findByLastName("Davis", pageable); + Page owners = this.owners.findByLastNameStartingWith("Davis", pageable); assertThat(owners).hasSize(2); - owners = this.owners.findByLastName("Daviss", pageable); + owners = this.owners.findByLastNameStartingWith("Daviss", pageable); assertThat(owners).isEmpty(); } @Test void shouldFindSingleOwnerWithPet() { - Owner owner = this.owners.findById(1); + Optional optionalOwner = this.owners.findById(1); + assertThat(optionalOwner).isPresent(); + Owner owner = optionalOwner.get(); assertThat(owner.getLastName()).startsWith("Franklin"); assertThat(owner.getPets()).hasSize(1); assertThat(owner.getPets().get(0).getType()).isNotNull(); @@ -102,7 +103,7 @@ void shouldFindSingleOwnerWithPet() { @Test @Transactional void shouldInsertOwner() { - Page owners = this.owners.findByLastName("Schultz", pageable); + Page owners = this.owners.findByLastNameStartingWith("Schultz", pageable); int found = (int) owners.getTotalElements(); Owner owner = new Owner(); @@ -114,14 +115,16 @@ void shouldInsertOwner() { this.owners.save(owner); assertThat(owner.getId()).isNotZero(); - owners = this.owners.findByLastName("Schultz", pageable); + owners = this.owners.findByLastNameStartingWith("Schultz", pageable); assertThat(owners.getTotalElements()).isEqualTo(found + 1); } @Test @Transactional void shouldUpdateOwner() { - Owner owner = this.owners.findById(1); + Optional optionalOwner = this.owners.findById(1); + assertThat(optionalOwner).isPresent(); + Owner owner = optionalOwner.get(); String oldLastName = owner.getLastName(); String newLastName = oldLastName + "X"; @@ -129,7 +132,9 @@ void shouldUpdateOwner() { this.owners.save(owner); // retrieving new name from database - owner = this.owners.findById(1); + optionalOwner = this.owners.findById(1); + assertThat(optionalOwner).isPresent(); + owner = optionalOwner.get(); assertThat(owner.getLastName()).isEqualTo(newLastName); } @@ -146,7 +151,10 @@ void shouldFindAllPetTypes() { @Test @Transactional void shouldInsertPetIntoDatabaseAndGenerateId() { - Owner owner6 = this.owners.findById(6); + Optional optionalOwner = this.owners.findById(6); + assertThat(optionalOwner).isPresent(); + Owner owner6 = optionalOwner.get(); + int found = owner6.getPets().size(); Pet pet = new Pet(); @@ -159,7 +167,9 @@ void shouldInsertPetIntoDatabaseAndGenerateId() { this.owners.save(owner6); - owner6 = this.owners.findById(6); + optionalOwner = this.owners.findById(6); + assertThat(optionalOwner).isPresent(); + owner6 = optionalOwner.get(); assertThat(owner6.getPets()).hasSize(found + 1); // checks that id has been generated pet = owner6.getPet("bowser"); @@ -169,7 +179,10 @@ void shouldInsertPetIntoDatabaseAndGenerateId() { @Test @Transactional void shouldUpdatePetName() { - Owner owner6 = this.owners.findById(6); + Optional optionalOwner = this.owners.findById(6); + assertThat(optionalOwner).isPresent(); + Owner owner6 = optionalOwner.get(); + Pet pet7 = owner6.getPet(7); String oldName = pet7.getName(); @@ -177,7 +190,9 @@ void shouldUpdatePetName() { pet7.setName(newName); this.owners.save(owner6); - owner6 = this.owners.findById(6); + optionalOwner = this.owners.findById(6); + assertThat(optionalOwner).isPresent(); + owner6 = optionalOwner.get(); pet7 = owner6.getPet(7); assertThat(pet7.getName()).isEqualTo(newName); } @@ -196,7 +211,10 @@ void shouldFindVets() { @Test @Transactional void shouldAddNewVisitForPet() { - Owner owner6 = this.owners.findById(6); + Optional optionalOwner = this.owners.findById(6); + assertThat(optionalOwner).isPresent(); + Owner owner6 = optionalOwner.get(); + Pet pet7 = owner6.getPet(7); int found = pet7.getVisits().size(); Visit visit = new Visit(); @@ -205,8 +223,6 @@ void shouldAddNewVisitForPet() { owner6.addVisit(pet7.getId(), visit); this.owners.save(owner6); - owner6 = this.owners.findById(6); - assertThat(pet7.getVisits()) // .hasSize(found + 1) // .allMatch(value -> value.getId() != null); @@ -214,7 +230,10 @@ void shouldAddNewVisitForPet() { @Test void shouldFindVisitsByPetId() { - Owner owner6 = this.owners.findById(6); + Optional optionalOwner = this.owners.findById(6); + assertThat(optionalOwner).isPresent(); + Owner owner6 = optionalOwner.get(); + Pet pet7 = owner6.getPet(7); Collection visits = pet7.getVisits(); diff --git a/src/test/java/org/springframework/samples/petclinic/system/CrashControllerTests.java b/src/test/java/org/springframework/samples/petclinic/system/CrashControllerTests.java index 09773aec..cc2ad674 100644 --- a/src/test/java/org/springframework/samples/petclinic/system/CrashControllerTests.java +++ b/src/test/java/org/springframework/samples/petclinic/system/CrashControllerTests.java @@ -30,7 +30,7 @@ // luck ((plain(st) UNIT test)! :) class CrashControllerTests { - CrashController testee = new CrashController(); + final CrashController testee = new CrashController(); @Test void testTriggerException() { diff --git a/src/test/java/org/springframework/samples/petclinic/vet/VetControllerTests.java b/src/test/java/org/springframework/samples/petclinic/vet/VetControllerTests.java index 20c3f46c..5fffeea4 100644 --- a/src/test/java/org/springframework/samples/petclinic/vet/VetControllerTests.java +++ b/src/test/java/org/springframework/samples/petclinic/vet/VetControllerTests.java @@ -22,11 +22,11 @@ import org.junit.jupiter.api.condition.DisabledInNativeImage; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.http.MediaType; import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @@ -48,7 +48,7 @@ class VetControllerTests { @Autowired private MockMvc mockMvc; - @MockBean + @MockitoBean private VetRepository vets; private Vet james() { diff --git a/src/test/jmeter/petclinic_test_plan.jmx b/src/test/jmeter/petclinic_test_plan.jmx index b5c18cb5..89c7bf21 100644 --- a/src/test/jmeter/petclinic_test_plan.jmx +++ b/src/test/jmeter/petclinic_test_plan.jmx @@ -156,8 +156,7 @@ - - ${CONTEXT_WEB}/webjars/bootstrap/5.3.3/dist/js/bootstrap.bundle.min.js + ${CONTEXT_WEB}/webjars/bootstrap/dist/js/bootstrap.bundle.min.js GET true false @@ -420,8 +419,7 @@ - - ${CONTEXT_WEB}/owners/${count}/pets/${petCount}/visits/new + ${CONTEXT_WEB}/owners/${count}/pets/${petCount}/visits/new GET true false @@ -458,8 +456,7 @@ - - ${CONTEXT_WEB}/owners/${count}/pets/${petCount}/visits/new + ${CONTEXT_WEB}/owners/${count}/pets/${petCount}/visits/new POST true false @@ -540,4 +537,4 @@ - \ No newline at end of file +