Compare commits

...

33 commits

Author SHA1 Message Date
2c6b644330
chore: temporarily remove unused workflows 2025-03-03 16:34:16 +01:00
34ef850730
feat(prometheus): add prometheus connector deps 2024-12-13 07:04:36 +01:00
feelgood1987
22caee3d03 feat: add Persian and Turkish localization files for application messages 2024-12-04 12:47:27 +00:00
Dave Syer
42e2c74b0b Add a Dockerfile for dev environments other than codespaces 2024-11-28 14:45:59 +00:00
Dave Syer
3b90fac983 Fix occasional stale volume in postgres tests
Fixes #1522
2024-11-28 14:40:54 +00:00
Dave Syer
300597fc6c Update tomestamp for reproducible builds 2024-11-28 14:40:54 +00:00
YiXuan Ding
40a41375e6 Add new test file
-<modify>: remove `@Transactional`.
-<add>: create unit-test file related to `PetValidator`.
-<refactor>: move pet objects initialization to `@BeforeEach` setup.
2024-11-28 09:52:29 +00:00
YiXuan Ding
214a8fb87f <fix>: rename the DI variable name in constructor. 2024-11-28 09:51:42 +00:00
Patrick Baumgartner
a0ba075bd8 Futher updates for Spring Boot 3.4 2024-11-27 11:24:18 +00:00
Dave Syer
91f55a4f71 Versionless webjars (again)
Spring Boot 3.4 supports the webjars-locator-lite which in turn
supports native images, so we are back to versionless URLs for
webjars assets in templates.
2024-11-27 08:32:35 +00:00
Dave Syer
9f1cda1c08 Update to Boot 3.4 2024-11-25 11:04:49 +00:00
Andrey Litvitski
317562a170 Implement K8S deployment testing using Kind in GitHub Actions 2024-11-11 21:41:36 +00:00
YiXuan Ding
1cad4124b7 Refactor code logic
<refactor>: remove useless logic cod.
<refactor>: detele useless annotation which is provided by Jpa.
<refactor>: refactor implement of `findByLastName`, use Jpa to simplify query.
2024-11-11 17:55:34 +00:00
YiXuan Ding
668629d5bd refactor OwnerRepository:
-<replace>: use `JpaRepository` to replace `Repository` in `OwnerRepository` class.
-<remove1>: remove `save()` method. JpaRepository provides it by default.
-<remove2>: remove `@Query` because in `Owner` class, the `@OneToMany` annotiation achieved `fetch` in query.
-<refactor1>: use `Optional<Owner>` to recieve the result from `findById()`, and if is null, throw `IllegalArugmentExpection`.
-<refactor2>: achieve the assert to judge return value in tests.
-<add>: add name to `@author` tag.
2024-11-10 09:14:42 +00:00
Andrey Litvitski
a3026bddbb Add Kubernetes support 2024-11-08 09:23:59 +00:00
YiXuan Ding
50866def72 Refactor the logic and add unit test
-<add>: add `@NotBlank` validation to pet's name.
-<refactor>: delete useless code and add unit test to check duplicate Pet name validation logic.
-<modify>: add `Id` to pet in unit test.
-<refactor>: classify unit test.

<modify>: adjust code format.
2024-11-07 22:34:46 +08:00
YiXuan Ding
14af47d4e5 Refactor:
- <optimize>: delete logic `add owner to model` because of the comment `@ModelAttribute("owner")`.
- <fix>: add logical judgment in ordet to avoid `owner` from `form` and `ownerId` from `url` mismatch.
2024-11-06 18:46:58 +00:00
YiXuan Ding
fdc40a7048 Fix harmless bugs.
- <fix>: use `equals` to replace `==` to compare `Integer` variable.
- <delete>: remove redundant 'toLowerCase()' method and simplify pet lookup logic.
- <update>: rewrite method `getName()` comments.
2024-11-05 16:31:25 +00:00
Patrick Baumgartner
a50bfb65bb Update Spring Boot release, adding Gradle build and cleanup
- Update Spring Boot release, Checkstyle, Mysql.
- Formatting pom.xml with sortpom-maven-plugin.
- Rename README to standard file name.
- Adding GitHub Action for Gradle.
2024-11-05 12:56:29 +00:00
João Bertholino
dff45cf70c fix: Temporarily removing accentuation from messages. 2024-11-05 08:27:02 +00:00
Dave Syer
90bbb98ea6 Update readme to refer to test main classes 2024-11-05 08:24:50 +00:00
João Bertholino
bbb237928f feat: Adds support for the Portuguese language. 2024-10-20 17:25:23 +01:00
ruabooe
912de1648e feat: add russian 2024-10-20 12:08:50 +01:00
Antoine Rey
62dbfa8e9a Remove the unnecessary includeFilters 2024-10-10 07:53:08 +01:00
Mousa Al Bateh
ae1bb8228c Minor code changes are the following:
-Removed unused variables from few files.
-Added null assertions in some tests.
-Removed unnecessary throw exceptions.
2024-10-10 07:50:06 +01:00
Guilherme Soares
fc442120ce fix(jmx): strip spaces before path 2024-09-30 21:42:09 +01:00
Patrick Baumgartner
f8001e0add Change chmod like other files 2024-09-30 21:41:01 +01:00
Patrick Baumgartner
608e2b6142 Update maven and gradle wrapper 2024-09-30 21:41:01 +01:00
Patrick Baumgartner
6fffe61b93 Updated db containers, and dependencies 2024-09-30 21:41:01 +01:00
ghost
2daa3993ee Removed unused imports
Made some variables as final
2024-09-24 20:37:48 +01:00
Sébastien Deleuze
d90e284621 Disable JPA Open Session In View
With the current codebase, it does not seems to be
needed anymore, so we disable it to provide better
performances.
2024-09-24 16:05:18 +01:00
Sébastien Deleuze
f6f923bd39 Upgrade to Spring Boot 3.3.4 2024-09-24 16:05:18 +01:00
Dave Syer
cabb74ed53 Update to Boot 3.3.3
Fixes #1658
2024-09-13 10:03:26 +01:00
46 changed files with 739 additions and 243 deletions

11
.devcontainer/Dockerfile Normal file
View file

@ -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'

View file

@ -1,5 +1,5 @@
# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time # 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 name: Java CI with Maven
@ -26,4 +26,4 @@ jobs:
distribution: 'adopt' distribution: 'adopt'
cache: maven cache: maven
- name: Build with Maven Wrapper - name: Build with Maven Wrapper
run: ./mvnw -B package run: ./mvnw -B verify

56
.gitignore vendored
View file

@ -1,17 +1,51 @@
target/* HELP.md
bin/* pom.xml.bak
build/* target/
.gradle/* !.mvn/wrapper/maven-wrapper.jar
.settings/* !**/src/main/**/target/
.classpath !**/src/test/**/target/
.project .gradle
.factorypath build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.attach_pid* .attach_pid*
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea .idea
*.iws
*.iml *.iml
/target *.ipr
.sts4-cache/ out/
.vscode !**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### SDK Man ###
.sdkmanrc
### CSS ###
_site/ _site/
*.css *.css
!petclinic.css !petclinic.css

View file

@ -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 wrapperVersion=3.3.2
distributionType=only-script 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

View file

@ -1,4 +1,4 @@
# Spring PetClinic Sample Application [![Build Status](https://github.com/spring-projects/spring-petclinic/actions/workflows/maven-build.yml/badge.svg)](https://github.com/spring-projects/spring-petclinic/actions/workflows/maven-build.yml) # Spring PetClinic Sample Application [![Build Status](https://github.com/spring-projects/spring-petclinic/actions/workflows/maven-build.yml/badge.svg)](https://github.com/spring-projects/spring-petclinic/actions/workflows/maven-build.yml)[![Build Status](https://github.com/spring-projects/spring-petclinic/actions/workflows/gradle-build.yml/badge.svg)](https://github.com/spring-projects/spring-petclinic/actions/workflows/gradle-build.yml)
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/spring-projects/spring-petclinic) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=7517918) [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/spring-projects/spring-petclinic) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=7517918)
@ -52,28 +52,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: You can start MySQL or PostgreSQL locally with whatever installer works for your OS or use docker:
```bash ```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 or
```bash ```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-projects/spring-petclinic/blob/main/src/main/resources/db/mysql/petclinic_db_setup_mysql.txt) Further documentation is provided for [MySQL](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/resources/db/mysql/petclinic_db_setup_mysql.txt)
and [PostgreSQL](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/resources/db/postgres/petclinic_db_setup_postgres.txt). and [PostgreSQL](https://github.com/spring-projects/spring-petclinic/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 ```bash
docker-compose --profile mysql up docker compose up mysql
``` ```
or or
```bash ```bash
docker-compose --profile postgres up docker compose up postgres
``` ```
## Test Applications ## Test Applications

View file

@ -1,10 +1,10 @@
plugins { plugins {
id 'java' id 'java'
id 'org.springframework.boot' version '3.3.2' id 'org.springframework.boot' version '3.4.0'
id 'io.spring.dependency-management' version '1.1.5' id 'io.spring.dependency-management' version '1.1.6'
id 'org.graalvm.buildtools.native' version '0.10.2' id 'org.graalvm.buildtools.native' version '0.10.3'
id 'org.cyclonedx.bom' version '1.8.2' id 'org.cyclonedx.bom' version '1.10.0'
id 'io.spring.javaformat' version '0.0.41' id 'io.spring.javaformat' version '0.0.43'
id "io.spring.nohttp" version "0.0.11" id "io.spring.nohttp" version "0.0.11"
} }
@ -15,7 +15,7 @@ apply plugin: 'io.spring.javaformat'
gradle.startParameter.excludedTaskNames += [ "checkFormatAot", "checkFormatAotTest" ] gradle.startParameter.excludedTaskNames += [ "checkFormatAot", "checkFormatAotTest" ]
group = 'org.springframework.samples' group = 'org.springframework.samples'
version = '3.3.0' version = '3.4.0'
java { java {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_17
@ -25,18 +25,26 @@ repositories {
mavenCentral() mavenCentral()
} }
ext.checkstyleVersion = "10.20.1"
ext.springJavaformatCheckstyleVersion = "0.0.43"
ext.webjarsLocatorLiteVersion = "1.0.1"
ext.webjarsFontawesomeVersion = "4.7.0" ext.webjarsFontawesomeVersion = "4.7.0"
ext.webjarsBootstrapVersion = "5.3.3" ext.webjarsBootstrapVersion = "5.3.3"
dependencies { dependencies {
// Workaround for AOT issue (https://github.com/spring-projects/spring-framework/pull/33949) -->
implementation 'io.projectreactor:reactor-core'
implementation 'org.springframework.boot:spring-boot-starter-cache' implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'io.micrometer:micrometer-registry-prometheus'
implementation 'javax.cache:cache-api' implementation 'javax.cache:cache-api'
implementation 'jakarta.xml.bind:jakarta.xml.bind-api' implementation 'jakarta.xml.bind:jakarta.xml.bind-api'
runtimeOnly 'org.springframework.boot:spring-boot-starter-actuator' 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:bootstrap:${webjarsBootstrapVersion}"
runtimeOnly "org.webjars.npm:font-awesome:${webjarsFontawesomeVersion}" runtimeOnly "org.webjars.npm:font-awesome:${webjarsFontawesomeVersion}"
runtimeOnly 'com.github.ben-manes.caffeine:caffeine' runtimeOnly 'com.github.ben-manes.caffeine:caffeine'
@ -49,8 +57,8 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-docker-compose' testImplementation 'org.springframework.boot:spring-boot-docker-compose'
testImplementation 'org.testcontainers:junit-jupiter' testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:mysql' testImplementation 'org.testcontainers:mysql'
checkstyle 'io.spring.javaformat:spring-javaformat-checkstyle:0.0.41' checkstyle "io.spring.javaformat:spring-javaformat-checkstyle:${springJavaformatCheckstyleVersion}"
checkstyle 'com.puppycrawl.tools:checkstyle:10.16.0' checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
} }
tasks.named('test') { tasks.named('test') {

View file

@ -1,6 +1,6 @@
services: services:
mysql: mysql:
image: mysql:8.4 image: mysql:9.1
ports: ports:
- "3306:3306" - "3306:3306"
environment: environment:
@ -11,15 +11,11 @@ services:
- MYSQL_DATABASE=petclinic - MYSQL_DATABASE=petclinic
volumes: volumes:
- "./conf.d:/etc/mysql/conf.d:ro" - "./conf.d:/etc/mysql/conf.d:ro"
profiles:
- mysql
postgres: postgres:
image: postgres:16.3 image: postgres:17.0
ports: ports:
- "5432:5432" - "5432:5432"
environment: environment:
- POSTGRES_PASSWORD=petclinic - POSTGRES_PASSWORD=petclinic
- POSTGRES_USER=petclinic - POSTGRES_USER=petclinic
- POSTGRES_DB=petclinic - POSTGRES_DB=petclinic
profiles:
- postgres

Binary file not shown.

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

7
gradlew vendored
View file

@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# SPDX-License-Identifier: Apache-2.0
#
############################################################################## ##############################################################################
# #
@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (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. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@ -84,7 +86,8 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # 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. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum

2
gradlew.bat vendored
View file

@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################

73
k8s/db.yml Normal file
View file

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

64
k8s/petclinic.yml Normal file
View file

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

112
pom.xml
View file

@ -1,17 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.samples</groupId>
<artifactId>spring-petclinic</artifactId>
<version>3.3.0-SNAPSHOT</version>
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version> <version>3.4.0</version>
<relativePath></relativePath>
</parent> </parent>
<groupId>org.springframework.samples</groupId>
<artifactId>spring-petclinic</artifactId>
<version>3.4.0-SNAPSHOT</version>
<name>petclinic</name> <name>petclinic</name>
<properties> <properties>
@ -20,20 +21,22 @@
<java.version>17</java.version> <java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- Important for reproducible builds. Update using e.g. ./mvnw versions:set -DnewVersion=... --> <!-- Important for reproducible builds. Update using e.g. ./mvnw versions:set
<project.build.outputTimestamp>2023-05-10T07:42:50Z</project.build.outputTimestamp> -DnewVersion=... -->
<project.build.outputTimestamp>2024-11-28T14:37:52Z</project.build.outputTimestamp>
<!-- Web dependencies --> <!-- Web dependencies -->
<webjars-locator.version>1.0.1</webjars-locator.version>
<webjars-bootstrap.version>5.3.3</webjars-bootstrap.version> <webjars-bootstrap.version>5.3.3</webjars-bootstrap.version>
<webjars-font-awesome.version>4.7.0</webjars-font-awesome.version> <webjars-font-awesome.version>4.7.0</webjars-font-awesome.version>
<checkstyle.version>10.16.0</checkstyle.version> <checkstyle.version>10.20.1</checkstyle.version>
<jacoco.version>0.8.12</jacoco.version> <jacoco.version>0.8.12</jacoco.version>
<libsass.version>0.2.29</libsass.version> <libsass.version>0.2.29</libsass.version>
<lifecycle-mapping>1.0.0</lifecycle-mapping> <lifecycle-mapping>1.0.0</lifecycle-mapping>
<maven-checkstyle.version>3.3.1</maven-checkstyle.version> <maven-checkstyle.version>3.6.0</maven-checkstyle.version>
<nohttp-checkstyle.version>0.0.11</nohttp-checkstyle.version> <nohttp-checkstyle.version>0.0.11</nohttp-checkstyle.version>
<spring-format.version>0.0.41</spring-format.version> <spring-format.version>0.0.43</spring-format.version>
</properties> </properties>
@ -68,6 +71,11 @@
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<!-- Workaround for AOT issue (https://github.com/spring-projects/spring-framework/pull/33949) -->
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>
<!-- Databases - Uses H2 by default --> <!-- Databases - Uses H2 by default -->
<dependency> <dependency>
@ -97,6 +105,11 @@
</dependency> </dependency>
<!-- Webjars --> <!-- Webjars -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-lite</artifactId>
<version>${webjars-locator.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.webjars.npm</groupId> <groupId>org.webjars.npm</groupId>
<artifactId>bootstrap</artifactId> <artifactId>bootstrap</artifactId>
@ -139,6 +152,11 @@
<artifactId>jakarta.xml.bind-api</artifactId> <artifactId>jakarta.xml.bind-api</artifactId>
</dependency> </dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>
@ -155,8 +173,9 @@
<configuration> <configuration>
<rules> <rules>
<requireJavaVersion> <requireJavaVersion>
<message>This build requires at least Java ${java.version}, update your JVM, and <message>This build requires at least Java ${java.version},
run the build again</message> update your JVM, and
run the build again</message>
<version>${java.version}</version> <version>${java.version}</version>
</requireJavaVersion> </requireJavaVersion>
</rules> </rules>
@ -170,10 +189,10 @@
<version>${spring-format.version}</version> <version>${spring-format.version}</version>
<executions> <executions>
<execution> <execution>
<phase>validate</phase>
<goals> <goals>
<goal>validate</goal> <goal>validate</goal>
</goals> </goals>
<phase>validate</phase>
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
@ -196,19 +215,17 @@
<executions> <executions>
<execution> <execution>
<id>nohttp-checkstyle-validation</id> <id>nohttp-checkstyle-validation</id>
<goals>
<goal>check</goal>
</goals>
<phase>validate</phase> <phase>validate</phase>
<configuration> <configuration>
<configLocation>src/checkstyle/nohttp-checkstyle.xml</configLocation> <configLocation>src/checkstyle/nohttp-checkstyle.xml</configLocation>
<sourceDirectories>${basedir}</sourceDirectories> <sourceDirectories>${basedir}</sourceDirectories>
<includes>**/*</includes> <includes>**/*</includes>
<excludes>**/.git/**/*,**/.idea/**/*,**/target/**/,**/.flattened-pom.xml,**/*.class</excludes> <excludes>**/.git/**/*,**/.idea/**/*,**/target/**/,**/.flattened-pom.xml,**/*.class</excludes>
<propertyExpansion> <propertyExpansion>config_loc=${basedir}/src/checkstyle/</propertyExpansion>
config_loc=${basedir}/src/checkstyle/
</propertyExpansion>
</configuration> </configuration>
<goals>
<goal>check</goal>
</goals>
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
@ -249,10 +266,10 @@
</execution> </execution>
<execution> <execution>
<id>report</id> <id>report</id>
<phase>prepare-package</phase>
<goals> <goals>
<goal>report</goal> <goal>report</goal>
</goals> </goals>
<phase>prepare-package</phase>
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
@ -270,13 +287,13 @@
<!-- Spring Boot Actuator displays sbom-related information if a CycloneDX SBOM file is <!-- Spring Boot Actuator displays sbom-related information if a CycloneDX SBOM file is
present at the classpath --> present at the classpath -->
<plugin> <plugin>
<?m2e ignore?>
<groupId>org.cyclonedx</groupId> <groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId> <artifactId>cyclonedx-maven-plugin</artifactId>
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
<licenses> <licenses>
<license> <license>
<name>Apache License, Version 2.0</name> <name>Apache License, Version 2.0</name>
@ -286,39 +303,38 @@
<repositories> <repositories>
<repository> <repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots> <snapshots>
<enabled>true</enabled> <enabled>true</enabled>
</snapshots> </snapshots>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
</repository> </repository>
<repository> <repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots> <snapshots>
<enabled>false</enabled> <enabled>false</enabled>
</snapshots> </snapshots>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository> </repository>
</repositories> </repositories>
<pluginRepositories> <pluginRepositories>
<pluginRepository> <pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots> <snapshots>
<enabled>true</enabled> <enabled>true</enabled>
</snapshots> </snapshots>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
</pluginRepository> </pluginRepository>
<pluginRepository> <pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots> <snapshots>
<enabled>false</enabled> <enabled>false</enabled>
</snapshots> </snapshots>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</pluginRepository> </pluginRepository>
</pluginRepositories> </pluginRepositories>
@ -333,11 +349,11 @@
<executions> <executions>
<execution> <execution>
<id>unpack</id> <id>unpack</id>
<?m2e execute onConfiguration,onIncremental?>
<phase>generate-resources</phase>
<goals> <goals>
<goal>unpack</goal> <goal>unpack</goal>
</goals> </goals>
<?m2e execute onConfiguration,onIncremental?>
<phase>generate-resources</phase>
<configuration> <configuration>
<artifactItems> <artifactItems>
<artifactItem> <artifactItem>
@ -356,21 +372,20 @@
<groupId>com.gitlab.haynes</groupId> <groupId>com.gitlab.haynes</groupId>
<artifactId>libsass-maven-plugin</artifactId> <artifactId>libsass-maven-plugin</artifactId>
<version>${libsass.version}</version> <version>${libsass.version}</version>
<configuration>
<inputPath>${basedir}/src/main/scss/</inputPath>
<outputPath>${basedir}/src/main/resources/static/resources/css/</outputPath>
<includePath>${project.build.directory}/webjars/META-INF/resources/webjars/bootstrap/${webjars-bootstrap.version}/scss/</includePath>
</configuration>
<executions> <executions>
<execution> <execution>
<phase>generate-resources</phase>
<?m2e execute onConfiguration,onIncremental?> <?m2e execute onConfiguration,onIncremental?>
<goals> <goals>
<goal>compile</goal> <goal>compile</goal>
</goals> </goals>
<phase>generate-resources</phase>
</execution> </execution>
</executions> </executions>
<configuration>
<inputPath>${basedir}/src/main/scss/</inputPath>
<outputPath>${basedir}/src/main/resources/static/resources/css/</outputPath>
<includePath>
${project.build.directory}/webjars/META-INF/resources/webjars/bootstrap/${webjars-bootstrap.version}/scss/</includePath>
</configuration>
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
@ -404,7 +419,7 @@
</goals> </goals>
</pluginExecutionFilter> </pluginExecutionFilter>
<action> <action>
<ignore /> <ignore></ignore>
</action> </action>
</pluginExecution> </pluginExecution>
<pluginExecution> <pluginExecution>
@ -417,7 +432,7 @@
</goals> </goals>
</pluginExecutionFilter> </pluginExecutionFilter>
<action> <action>
<ignore /> <ignore></ignore>
</action> </action>
</pluginExecution> </pluginExecution>
<pluginExecution> <pluginExecution>
@ -430,7 +445,7 @@
</goals> </goals>
</pluginExecutionFilter> </pluginExecutionFilter>
<action> <action>
<ignore /> <ignore></ignore>
</action> </action>
</pluginExecution> </pluginExecution>
</pluginExecutions> </pluginExecutions>
@ -442,5 +457,4 @@
</build> </build>
</profile> </profile>
</profiles> </profiles>
</project> </project>

View file

@ -18,11 +18,7 @@ package org.springframework.samples.petclinic;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import java.util.Locale;
/** /**
* PetClinic Spring Boot Application. * PetClinic Spring Boot Application.

View file

@ -28,7 +28,6 @@ public class PetClinicRuntimeHints implements RuntimeHintsRegistrar {
public void registerHints(RuntimeHints hints, ClassLoader classLoader) { public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.resources().registerPattern("db/*"); // https://github.com/spring-projects/spring-boot/issues/32654 hints.resources().registerPattern("db/*"); // https://github.com/spring-projects/spring-boot/issues/32654
hints.resources().registerPattern("messages/*"); hints.resources().registerPattern("messages/*");
hints.resources().registerPattern("META-INF/resources/webjars/*");
hints.resources().registerPattern("mysql-default-conf"); hints.resources().registerPattern("mysql-default-conf");
hints.serialization().registerType(BaseEntity.class); hints.serialization().registerType(BaseEntity.class);
hints.serialization().registerType(Person.class); hints.serialization().registerType(Person.class);

View file

@ -17,6 +17,7 @@ package org.springframework.samples.petclinic.model;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass; import jakarta.persistence.MappedSuperclass;
import jakarta.validation.constraints.NotBlank;
/** /**
* Simple JavaBean domain object adds a name property to <code>BaseEntity</code>. Used as * Simple JavaBean domain object adds a name property to <code>BaseEntity</code>. Used as
@ -24,11 +25,13 @@ import jakarta.persistence.MappedSuperclass;
* *
* @author Ken Krebs * @author Ken Krebs
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Wick Dynex
*/ */
@MappedSuperclass @MappedSuperclass
public class NamedEntity extends BaseEntity { public class NamedEntity extends BaseEntity {
@Column(name = "name") @Column(name = "name")
@NotBlank
private String name; private String name;
public String getName() { public String getName() {

View file

@ -41,6 +41,7 @@ import jakarta.validation.constraints.NotBlank;
* @author Sam Brannen * @author Sam Brannen
* @author Michael Isvy * @author Michael Isvy
* @author Oliver Drotbohm * @author Oliver Drotbohm
* @author Wick Dynex
*/ */
@Entity @Entity
@Table(name = "owners") @Table(name = "owners")
@ -62,7 +63,7 @@ public class Owner extends Person {
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "owner_id") @JoinColumn(name = "owner_id")
@OrderBy("name") @OrderBy("name")
private List<Pet> pets = new ArrayList<>(); private final List<Pet> pets = new ArrayList<>();
public String getAddress() { public String getAddress() {
return this.address; return this.address;
@ -101,7 +102,7 @@ public class Owner extends Person {
/** /**
* Return the Pet with the given name, or null if none found for this Owner. * Return the Pet with the given name, or null if none found for this Owner.
* @param name to test * @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) { public Pet getPet(String name) {
return getPet(name, false); return getPet(name, false);
@ -110,7 +111,7 @@ public class Owner extends Person {
/** /**
* Return the Pet with the given id, or null if none found for this Owner. * Return the Pet with the given id, or null if none found for this Owner.
* @param id to test * @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) { public Pet getPet(Integer id) {
for (Pet pet : getPets()) { for (Pet pet : getPets()) {
@ -127,10 +128,10 @@ public class Owner extends Person {
/** /**
* Return the Pet with the given name, or null if none found for this Owner. * Return the Pet with the given name, or null if none found for this Owner.
* @param name to test * @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) { public Pet getPet(String name, boolean ignoreNew) {
name = name.toLowerCase();
for (Pet pet : getPets()) { for (Pet pet : getPets()) {
String compName = pet.getName(); String compName = pet.getName();
if (compName != null && compName.equalsIgnoreCase(name)) { if (compName != null && compName.equalsIgnoreCase(name)) {

View file

@ -16,8 +16,8 @@
package org.springframework.samples.petclinic.owner; package org.springframework.samples.petclinic.owner;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
@ -41,6 +41,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
* @author Ken Krebs * @author Ken Krebs
* @author Arjen Poutsma * @author Arjen Poutsma
* @author Michael Isvy * @author Michael Isvy
* @author Wick Dynex
*/ */
@Controller @Controller
class OwnerController { class OwnerController {
@ -49,8 +50,8 @@ class OwnerController {
private final OwnerRepository owners; private final OwnerRepository owners;
public OwnerController(OwnerRepository clinicService) { public OwnerController(OwnerRepository owners) {
this.owners = clinicService; this.owners = owners;
} }
@InitBinder @InitBinder
@ -60,13 +61,14 @@ class OwnerController {
@ModelAttribute("owner") @ModelAttribute("owner")
public Owner findOwner(@PathVariable(name = "ownerId", required = false) Integer ownerId) { 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") @GetMapping("/owners/new")
public String initCreationForm(Map<String, Object> model) { public String initCreationForm() {
Owner owner = new Owner();
model.put("owner", owner);
return VIEWS_OWNER_CREATE_OR_UPDATE_FORM; return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
} }
@ -125,13 +127,11 @@ class OwnerController {
private Page<Owner> findPaginatedForOwnersLastName(int page, String lastname) { private Page<Owner> findPaginatedForOwnersLastName(int page, String lastname) {
int pageSize = 5; int pageSize = 5;
Pageable pageable = PageRequest.of(page - 1, pageSize); Pageable pageable = PageRequest.of(page - 1, pageSize);
return owners.findByLastName(lastname, pageable); return owners.findByLastNameStartingWith(lastname, pageable);
} }
@GetMapping("/owners/{ownerId}/edit") @GetMapping("/owners/{ownerId}/edit")
public String initUpdateOwnerForm(@PathVariable("ownerId") int ownerId, Model model) { public String initUpdateOwnerForm() {
Owner owner = this.owners.findById(ownerId);
model.addAttribute(owner);
return VIEWS_OWNER_CREATE_OR_UPDATE_FORM; return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
} }
@ -143,6 +143,12 @@ class OwnerController {
return VIEWS_OWNER_CREATE_OR_UPDATE_FORM; 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); owner.setId(ownerId);
this.owners.save(owner); this.owners.save(owner);
redirectAttributes.addFlashAttribute("message", "Owner Values Updated"); redirectAttributes.addFlashAttribute("message", "Owner Values Updated");
@ -157,7 +163,9 @@ class OwnerController {
@GetMapping("/owners/{ownerId}") @GetMapping("/owners/{ownerId}")
public ModelAndView showOwner(@PathVariable("ownerId") int ownerId) { public ModelAndView showOwner(@PathVariable("ownerId") int ownerId) {
ModelAndView mav = new ModelAndView("owners/ownerDetails"); ModelAndView mav = new ModelAndView("owners/ownerDetails");
Owner owner = this.owners.findById(ownerId); Optional<Owner> 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); mav.addObject(owner);
return mav; return mav;
} }

View file

@ -16,12 +16,13 @@
package org.springframework.samples.petclinic.owner; package org.springframework.samples.petclinic.owner;
import java.util.List; import java.util.List;
import java.util.Optional;
import jakarta.annotation.Nonnull;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; 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; import org.springframework.transaction.annotation.Transactional;
/** /**
@ -34,15 +35,15 @@ import org.springframework.transaction.annotation.Transactional;
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Sam Brannen * @author Sam Brannen
* @author Michael Isvy * @author Michael Isvy
* @author Wick Dynex
*/ */
public interface OwnerRepository extends Repository<Owner, Integer> { public interface OwnerRepository extends JpaRepository<Owner, Integer> {
/** /**
* Retrieve all {@link PetType}s from the data store. * Retrieve all {@link PetType}s from the data store.
* @return a Collection of {@link PetType}s. * @return a Collection of {@link PetType}s.
*/ */
@Query("SELECT ptype FROM PetType ptype ORDER BY ptype.name") @Query("SELECT ptype FROM PetType ptype ORDER BY ptype.name")
@Transactional(readOnly = true)
List<PetType> findPetTypes(); List<PetType> findPetTypes();
/** /**
@ -52,31 +53,26 @@ public interface OwnerRepository extends Repository<Owner, Integer> {
* @return a Collection of matching {@link Owner}s (or an empty Collection if none * @return a Collection of matching {@link Owner}s (or an empty Collection if none
* found) * found)
*/ */
Page<Owner> findByLastNameStartingWith(String lastName, Pageable pageable);
@Query("SELECT DISTINCT owner FROM Owner owner left join owner.pets WHERE owner.lastName LIKE :lastName% ")
@Transactional(readOnly = true)
Page<Owner> findByLastName(@Param("lastName") String lastName, Pageable pageable);
/** /**
* Retrieve an {@link Owner} from the data store by id. * Retrieve an {@link Owner} from the data store by id.
* <p>
* 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}.
* </p>
* @param id the id to search for * @param id the id to search for
* @return the {@link Owner} if found * @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)
*/ */
@Query("SELECT owner FROM Owner owner left join fetch owner.pets WHERE owner.id =:id") Optional<Owner> findById(@Nonnull Integer 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
*/
void save(Owner owner);
/** /**
* Returns all the owners from data store * Returns all the owners from data store
**/ **/
@Query("SELECT owner FROM Owner owner")
@Transactional(readOnly = true)
Page<Owner> findAll(Pageable pageable); Page<Owner> findAll(Pageable pageable);
} }

View file

@ -39,6 +39,7 @@ import jakarta.persistence.Table;
* @author Ken Krebs * @author Ken Krebs
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Sam Brannen * @author Sam Brannen
* @author Wick Dynex
*/ */
@Entity @Entity
@Table(name = "pets") @Table(name = "pets")
@ -55,7 +56,7 @@ public class Pet extends NamedEntity {
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "pet_id") @JoinColumn(name = "pet_id")
@OrderBy("visit_date ASC") @OrderBy("visit_date ASC")
private Set<Visit> visits = new LinkedHashSet<>(); private final Set<Visit> visits = new LinkedHashSet<>();
public void setBirthDate(LocalDate birthDate) { public void setBirthDate(LocalDate birthDate) {
this.birthDate = birthDate; this.birthDate = birthDate;

View file

@ -17,6 +17,7 @@ package org.springframework.samples.petclinic.owner;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Collection; import java.util.Collection;
import java.util.Optional;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap; import org.springframework.ui.ModelMap;
@ -37,6 +38,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Ken Krebs * @author Ken Krebs
* @author Arjen Poutsma * @author Arjen Poutsma
* @author Wick Dynex
*/ */
@Controller @Controller
@RequestMapping("/owners/{ownerId}") @RequestMapping("/owners/{ownerId}")
@ -57,11 +59,9 @@ class PetController {
@ModelAttribute("owner") @ModelAttribute("owner")
public Owner findOwner(@PathVariable("ownerId") int ownerId) { public Owner findOwner(@PathVariable("ownerId") int ownerId) {
Optional<Owner> optionalOwner = this.owners.findById(ownerId);
Owner owner = this.owners.findById(ownerId); Owner owner = optionalOwner.orElseThrow(() -> new IllegalArgumentException(
if (owner == null) { "Owner not found with id: " + ownerId + ". Please ensure the ID is correct "));
throw new IllegalArgumentException("Owner ID not found: " + ownerId);
}
return owner; return owner;
} }
@ -73,10 +73,9 @@ class PetController {
return new Pet(); return new Pet();
} }
Owner owner = this.owners.findById(ownerId); Optional<Owner> optionalOwner = this.owners.findById(ownerId);
if (owner == null) { Owner owner = optionalOwner.orElseThrow(() -> new IllegalArgumentException(
throw new IllegalArgumentException("Owner ID not found: " + ownerId); "Owner not found with id: " + ownerId + ". Please ensure the ID is correct "));
}
return owner.getPet(petId); return owner.getPet(petId);
} }
@ -94,51 +93,46 @@ class PetController {
public String initCreationForm(Owner owner, ModelMap model) { public String initCreationForm(Owner owner, ModelMap model) {
Pet pet = new Pet(); Pet pet = new Pet();
owner.addPet(pet); owner.addPet(pet);
model.put("pet", pet);
return VIEWS_PETS_CREATE_OR_UPDATE_FORM; return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
} }
@PostMapping("/pets/new") @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) { 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"); result.rejectValue("name", "duplicate", "already exists");
}
LocalDate currentDate = LocalDate.now(); LocalDate currentDate = LocalDate.now();
if (pet.getBirthDate() != null && pet.getBirthDate().isAfter(currentDate)) { if (pet.getBirthDate() != null && pet.getBirthDate().isAfter(currentDate)) {
result.rejectValue("birthDate", "typeMismatch.birthDate"); result.rejectValue("birthDate", "typeMismatch.birthDate");
} }
owner.addPet(pet);
if (result.hasErrors()) { if (result.hasErrors()) {
model.put("pet", pet);
return VIEWS_PETS_CREATE_OR_UPDATE_FORM; return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
} }
owner.addPet(pet);
this.owners.save(owner); this.owners.save(owner);
redirectAttributes.addFlashAttribute("message", "New Pet has been Added"); redirectAttributes.addFlashAttribute("message", "New Pet has been Added");
return "redirect:/owners/{ownerId}"; return "redirect:/owners/{ownerId}";
} }
@GetMapping("/pets/{petId}/edit") @GetMapping("/pets/{petId}/edit")
public String initUpdateForm(Owner owner, @PathVariable("petId") int petId, ModelMap model, public String initUpdateForm() {
RedirectAttributes redirectAttributes) {
Pet pet = owner.getPet(petId);
model.put("pet", pet);
return VIEWS_PETS_CREATE_OR_UPDATE_FORM; return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
} }
@PostMapping("/pets/{petId}/edit") @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) { RedirectAttributes redirectAttributes) {
String petName = pet.getName(); String petName = pet.getName();
// checking if the pet name already exist for the owner // checking if the pet name already exist for the owner
if (StringUtils.hasText(petName)) { if (StringUtils.hasText(petName)) {
Pet existingPet = owner.getPet(petName.toLowerCase(), false); Pet existingPet = owner.getPet(petName, false);
if (existingPet != null && existingPet.getId() != pet.getId()) { if (existingPet != null && !existingPet.getId().equals(pet.getId())) {
result.rejectValue("name", "duplicate", "already exists"); result.rejectValue("name", "duplicate", "already exists");
} }
} }
@ -149,7 +143,6 @@ class PetController {
} }
if (result.hasErrors()) { if (result.hasErrors()) {
model.put("pet", pet);
return VIEWS_PETS_CREATE_OR_UPDATE_FORM; return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
} }

View file

View file

@ -16,6 +16,7 @@
package org.springframework.samples.petclinic.owner; package org.springframework.samples.petclinic.owner;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResult;
@ -35,6 +36,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
* @author Arjen Poutsma * @author Arjen Poutsma
* @author Michael Isvy * @author Michael Isvy
* @author Dave Syer * @author Dave Syer
* @author Wick Dynex
*/ */
@Controller @Controller
class VisitController { class VisitController {
@ -60,7 +62,9 @@ class VisitController {
@ModelAttribute("visit") @ModelAttribute("visit")
public Visit loadPetWithVisit(@PathVariable("ownerId") int ownerId, @PathVariable("petId") int petId, public Visit loadPetWithVisit(@PathVariable("ownerId") int ownerId, @PathVariable("petId") int petId,
Map<String, Object> model) { Map<String, Object> model) {
Owner owner = this.owners.findById(ownerId); Optional<Owner> 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); Pet pet = owner.getPet(petId);
model.put("pet", pet); model.put("pet", pet);

View file

@ -57,10 +57,6 @@ public class Vet extends Person {
return this.specialties; return this.specialties;
} }
protected void setSpecialtiesInternal(Set<Specialty> specialties) {
this.specialties = specialties;
}
@XmlElement @XmlElement
public List<Specialty> getSpecialties() { public List<Specialty> getSpecialties() {
List<Specialty> sortedSpecs = new ArrayList<>(getSpecialtiesInternal()); List<Specialty> sortedSpecs = new ArrayList<>(getSpecialtiesInternal());

View file

@ -37,8 +37,8 @@ class VetController {
private final VetRepository vetRepository; private final VetRepository vetRepository;
public VetController(VetRepository clinicService) { public VetController(VetRepository vetRepository) {
this.vetRepository = clinicService; this.vetRepository = vetRepository;
} }
@GetMapping("/vets.html") @GetMapping("/vets.html")

View file

@ -8,7 +8,7 @@ spring.thymeleaf.mode=HTML
# JPA # JPA
spring.jpa.hibernate.ddl-auto=none spring.jpa.hibernate.ddl-auto=none
spring.jpa.open-in-view=true spring.jpa.open-in-view=false
# Internationalization # Internationalization
spring.messages.basename=messages/messages spring.messages.basename=messages/messages

View file

@ -0,0 +1,9 @@
welcome=خوش آمدید
required=الزامی
notFound=یافت نشد
duplicate=قبلا استفاده شده
nonNumeric=باید عددی باشد
duplicateFormSubmission=ارسال تکراری فرم مجاز نیست
typeMismatch.date=تاریخ نامعتبر
typeMismatch.birthDate=تاریخ تولد نامعتبر

View file

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

View file

@ -0,0 +1,9 @@
welcome=Добро пожаловать
required=необходимо
notFound=не найдено
duplicate=уже используется
nonNumeric=должно быть все числовое значение
duplicateFormSubmission=Дублирование формы не допускается
typeMismatch.date=неправильная даные
typeMismatch.birthDate=неправильная дата

View file

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

4
src/main/resources/templates/fragments/layout.html Executable file → Normal file
View file

@ -17,7 +17,7 @@
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script> <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]--> <![endif]-->
<link th:href="@{/webjars/font-awesome/4.7.0/css/font-awesome.min.css}" rel="stylesheet"> <link th:href="@{/webjars/font-awesome/css/font-awesome.min.css}" rel="stylesheet">
<link rel="stylesheet" th:href="@{/resources/css/petclinic.css}" /> <link rel="stylesheet" th:href="@{/resources/css/petclinic.css}" />
</head> </head>
@ -87,7 +87,7 @@
</div> </div>
</div> </div>
<script th:src="@{/webjars/bootstrap/5.3.3/dist/js/bootstrap.bundle.min.js}"></script> <script th:src="@{/webjars/bootstrap/dist/js/bootstrap.bundle.min.js}"></script>
</body> </body>

View file

@ -46,7 +46,7 @@ class MySqlIntegrationTests {
@ServiceConnection @ServiceConnection
@Container @Container
static MySQLContainer<?> container = new MySQLContainer<>("mysql:8.4"); static MySQLContainer<?> container = new MySQLContainer<>("mysql:9.1");
@LocalServerPort @LocalServerPort
int port; int port;
@ -58,7 +58,7 @@ class MySqlIntegrationTests {
private RestTemplateBuilder builder; private RestTemplateBuilder builder;
@Test @Test
void testFindAll() throws Exception { void testFindAll() {
vets.findAll(); vets.findAll();
vets.findAll(); // served from cache vets.findAll(); // served from cache
} }

View file

@ -36,7 +36,7 @@ public class MysqlTestApplication {
@Profile("mysql") @Profile("mysql")
@Bean @Bean
static MySQLContainer<?> container() { static MySQLContainer<?> container() {
return new MySQLContainer<>("mysql:8.4"); return new MySQLContainer<>("mysql:9.1");
} }
public static void main(String[] args) { public static void main(String[] args) {

View file

@ -44,7 +44,7 @@ public class PetClinicIntegrationTests {
private RestTemplateBuilder builder; private RestTemplateBuilder builder;
@Test @Test
void testFindAll() throws Exception { void testFindAll() {
vets.findAll(); vets.findAll();
vets.findAll(); // served from cache vets.findAll(); // served from cache
} }

View file

@ -17,6 +17,7 @@
package org.springframework.samples.petclinic; package org.springframework.samples.petclinic;
import static org.assertj.core.api.Assertions.assertThat; 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 static org.junit.jupiter.api.Assumptions.assumeTrue;
import java.util.Arrays; import java.util.Arrays;
@ -48,7 +49,7 @@ import org.springframework.web.client.RestTemplate;
import org.testcontainers.DockerClientFactory; import org.testcontainers.DockerClientFactory;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "spring.docker.compose.skip.in-tests=false", // @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") @ActiveProfiles("postgres")
@DisabledInNativeImage @DisabledInNativeImage
public class PostgresIntegrationTests { public class PostgresIntegrationTests {
@ -71,7 +72,7 @@ public class PostgresIntegrationTests {
new SpringApplicationBuilder(PetClinicApplication.class) // new SpringApplicationBuilder(PetClinicApplication.class) //
.profiles("postgres") // .profiles("postgres") //
.properties( // .properties( //
"spring.docker.compose.profiles.active=postgres" // "spring.docker.compose.start.arguments=postgres" //
) // ) //
.listeners(new PropertiesLogger()) // .listeners(new PropertiesLogger()) //
.run(args); .run(args);
@ -114,7 +115,16 @@ public class PostgresIntegrationTests {
Arrays.sort(names); Arrays.sort(names);
for (String name : names) { for (String name : names) {
String resolved = environment.getProperty(name); 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)) { if (resolved.equals(value)) {
log.info(name + "=" + resolved); log.info(name + "=" + resolved);
} }

View file

@ -20,17 +20,18 @@ import org.assertj.core.util.Lists;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledInNativeImage; import org.junit.jupiter.api.condition.DisabledInNativeImage;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 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.Page;
import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.test.context.aot.DisabledInAotMode; 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.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Optional;
import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThan;
@ -43,16 +44,16 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given; 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.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 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.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
/** /**
* Test class for {@link OwnerController} * Test class for {@link OwnerController}
* *
* @author Colin But * @author Colin But
* @author Wick Dynex
*/ */
@WebMvcTest(OwnerController.class) @WebMvcTest(OwnerController.class)
@DisabledInNativeImage @DisabledInNativeImage
@ -64,7 +65,7 @@ class OwnerControllerTests {
@Autowired @Autowired
private MockMvc mockMvc; private MockMvc mockMvc;
@MockBean @MockitoBean
private OwnerRepository owners; private OwnerRepository owners;
private Owner george() { private Owner george() {
@ -90,12 +91,12 @@ class OwnerControllerTests {
void setup() { void setup() {
Owner george = george(); 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))); .willReturn(new PageImpl<>(Lists.newArrayList(george)));
given(this.owners.findAll(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 visit = new Visit();
visit.setDate(LocalDate.now()); visit.setDate(LocalDate.now());
george.getPet("Max").getVisits().add(visit); george.getPet("Max").getVisits().add(visit);
@ -143,14 +144,14 @@ class OwnerControllerTests {
@Test @Test
void testProcessFindFormSuccess() throws Exception { void testProcessFindFormSuccess() throws Exception {
Page<Owner> tasks = new PageImpl<>(Lists.newArrayList(george(), new Owner())); Page<Owner> 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")); mockMvc.perform(get("/owners?page=1")).andExpect(status().isOk()).andExpect(view().name("owners/ownersList"));
} }
@Test @Test
void testProcessFindFormByLastName() throws Exception { void testProcessFindFormByLastName() throws Exception {
Page<Owner> tasks = new PageImpl<>(Lists.newArrayList(george())); Page<Owner> 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")) mockMvc.perform(get("/owners?page=1").param("lastName", "Franklin"))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(view().name("redirect:/owners/" + TEST_OWNER_ID)); .andExpect(view().name("redirect:/owners/" + TEST_OWNER_ID));
@ -159,7 +160,7 @@ class OwnerControllerTests {
@Test @Test
void testProcessFindFormNoOwnersFound() throws Exception { void testProcessFindFormNoOwnersFound() throws Exception {
Page<Owner> tasks = new PageImpl<>(Lists.newArrayList()); Page<Owner> 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")) mockMvc.perform(get("/owners?page=1").param("lastName", "Unknown Surname"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(model().attributeHasFieldErrors("owner", "lastName")) .andExpect(model().attributeHasFieldErrors("owner", "lastName"))
@ -229,4 +230,24 @@ class OwnerControllerTests {
.andExpect(view().name("owners/ownerDetails")); .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"));
}
} }

View file

@ -18,16 +18,20 @@ package org.springframework.samples.petclinic.owner;
import org.assertj.core.util.Lists; import org.assertj.core.util.Lists;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledInNativeImage; import org.junit.jupiter.api.condition.DisabledInNativeImage;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 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.ComponentScan;
import org.springframework.context.annotation.FilterType; import org.springframework.context.annotation.FilterType;
import org.springframework.test.context.aot.DisabledInAotMode; 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.MockMvc;
import java.time.LocalDate;
import java.util.Optional;
import static org.mockito.BDDMockito.given; 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.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@ -39,6 +43,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
* Test class for the {@link PetController} * Test class for the {@link PetController}
* *
* @author Colin But * @author Colin But
* @author Wick Dynex
*/ */
@WebMvcTest(value = PetController.class, @WebMvcTest(value = PetController.class,
includeFilters = @ComponentScan.Filter(value = PetTypeFormatter.class, type = FilterType.ASSIGNABLE_TYPE)) includeFilters = @ComponentScan.Filter(value = PetTypeFormatter.class, type = FilterType.ASSIGNABLE_TYPE))
@ -53,7 +58,7 @@ class PetControllerTests {
@Autowired @Autowired
private MockMvc mockMvc; private MockMvc mockMvc;
@MockBean @MockitoBean
private OwnerRepository owners; private OwnerRepository owners;
@BeforeEach @BeforeEach
@ -62,11 +67,17 @@ class PetControllerTests {
cat.setId(3); cat.setId(3);
cat.setName("hamster"); cat.setName("hamster");
given(this.owners.findPetTypes()).willReturn(Lists.newArrayList(cat)); given(this.owners.findPetTypes()).willReturn(Lists.newArrayList(cat));
Owner owner = new Owner(); Owner owner = new Owner();
Pet pet = new Pet(); Pet pet = new Pet();
Pet dog = new Pet();
owner.addPet(pet); owner.addPet(pet);
owner.addPet(dog);
pet.setId(TEST_PET_ID); 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 @Test
@ -87,25 +98,72 @@ class PetControllerTests {
.andExpect(view().name("redirect:/owners/{ownerId}")); .andExpect(view().name("redirect:/owners/{ownerId}"));
} }
@Test @Nested
void testProcessCreationFormHasErrors() throws Exception { class ProcessCreationFormHasErrors {
mockMvc
.perform(post("/owners/{ownerId}/pets/new", TEST_OWNER_ID).param("name", "Betty") @Test
.param("birthDate", "2015-02-12")) void testProcessCreationFormWithBlankName() throws Exception {
.andExpect(model().attributeHasNoErrors("owner")) mockMvc
.andExpect(model().attributeHasErrors("pet")) .perform(post("/owners/{ownerId}/pets/new", TEST_OWNER_ID).param("name", "\t \n")
.andExpect(model().attributeHasFieldErrors("pet", "type")) .param("birthDate", "2015-02-12"))
.andExpect(model().attributeHasFieldErrorCode("pet", "type", "required")) .andExpect(model().attributeHasNoErrors("owner"))
.andExpect(status().isOk()) .andExpect(model().attributeHasErrors("pet"))
.andExpect(view().name("pets/createOrUpdatePetForm")); .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 @Test
@ -118,15 +176,33 @@ class PetControllerTests {
.andExpect(view().name("redirect:/owners/{ownerId}")); .andExpect(view().name("redirect:/owners/{ownerId}"));
} }
@Test @Nested
void testProcessUpdateFormHasErrors() throws Exception { class ProcessUpdateFormHasErrors {
mockMvc
.perform(post("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID).param("name", "Betty") @Test
.param("birthDate", "2015/02/12")) void testProcessUpdateFormWithInvalidBirthDate() throws Exception {
.andExpect(model().attributeHasNoErrors("owner")) mockMvc
.andExpect(model().attributeHasErrors("pet")) .perform(post("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID).param("name", " ")
.andExpect(status().isOk()) .param("birthDate", "2015/02/12"))
.andExpect(view().name("pets/createOrUpdatePetForm")); .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"));
}
} }
} }

View file

@ -68,7 +68,7 @@ class PetTypeFormatterTests {
} }
@Test @Test
void shouldThrowParseException() throws ParseException { void shouldThrowParseException() {
given(this.pets.findPetTypes()).willReturn(makePetTypes()); given(this.pets.findPetTypes()).willReturn(makePetTypes());
Assertions.assertThrows(ParseException.class, () -> { Assertions.assertThrows(ParseException.class, () -> {
petTypeFormatter.parse("Fish", Locale.ENGLISH); petTypeFormatter.parse("Fish", Locale.ENGLISH);

View file

@ -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"));
}
}
}

View file

@ -28,14 +28,17 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledInNativeImage; import org.junit.jupiter.api.condition.DisabledInNativeImage;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 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.aot.DisabledInAotMode;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import java.util.Optional;
/** /**
* Test class for {@link VisitController} * Test class for {@link VisitController}
* *
* @author Colin But * @author Colin But
* @author Wick Dynex
*/ */
@WebMvcTest(VisitController.class) @WebMvcTest(VisitController.class)
@DisabledInNativeImage @DisabledInNativeImage
@ -49,7 +52,7 @@ class VisitControllerTests {
@Autowired @Autowired
private MockMvc mockMvc; private MockMvc mockMvc;
@MockBean @MockitoBean
private OwnerRepository owners; private OwnerRepository owners;
@BeforeEach @BeforeEach
@ -58,7 +61,7 @@ class VisitControllerTests {
Pet pet = new Pet(); Pet pet = new Pet();
owner.addPet(pet); owner.addPet(pet);
pet.setId(TEST_PET_ID); 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 @Test

View file

@ -20,13 +20,13 @@ import static org.assertj.core.api.Assertions.assertThat;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Collection; import java.util.Collection;
import java.util.Optional;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 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.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.samples.petclinic.owner.Owner; import org.springframework.samples.petclinic.owner.Owner;
@ -36,7 +36,6 @@ import org.springframework.samples.petclinic.owner.PetType;
import org.springframework.samples.petclinic.owner.Visit; import org.springframework.samples.petclinic.owner.Visit;
import org.springframework.samples.petclinic.vet.Vet; import org.springframework.samples.petclinic.vet.Vet;
import org.springframework.samples.petclinic.vet.VetRepository; import org.springframework.samples.petclinic.vet.VetRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
/** /**
@ -67,7 +66,7 @@ import org.springframework.transaction.annotation.Transactional;
* @author Michael Isvy * @author Michael Isvy
* @author Dave Syer * @author Dave Syer
*/ */
@DataJpaTest(includeFilters = @ComponentScan.Filter(Service.class)) @DataJpaTest
// Ensure that if the mysql profile is active we connect to the real database: // Ensure that if the mysql profile is active we connect to the real database:
@AutoConfigureTestDatabase(replace = Replace.NONE) @AutoConfigureTestDatabase(replace = Replace.NONE)
// @TestPropertySource("/application-postgres.properties") // @TestPropertySource("/application-postgres.properties")
@ -83,16 +82,18 @@ class ClinicServiceTests {
@Test @Test
void shouldFindOwnersByLastName() { void shouldFindOwnersByLastName() {
Page<Owner> owners = this.owners.findByLastName("Davis", pageable); Page<Owner> owners = this.owners.findByLastNameStartingWith("Davis", pageable);
assertThat(owners).hasSize(2); assertThat(owners).hasSize(2);
owners = this.owners.findByLastName("Daviss", pageable); owners = this.owners.findByLastNameStartingWith("Daviss", pageable);
assertThat(owners).isEmpty(); assertThat(owners).isEmpty();
} }
@Test @Test
void shouldFindSingleOwnerWithPet() { void shouldFindSingleOwnerWithPet() {
Owner owner = this.owners.findById(1); Optional<Owner> optionalOwner = this.owners.findById(1);
assertThat(optionalOwner).isPresent();
Owner owner = optionalOwner.get();
assertThat(owner.getLastName()).startsWith("Franklin"); assertThat(owner.getLastName()).startsWith("Franklin");
assertThat(owner.getPets()).hasSize(1); assertThat(owner.getPets()).hasSize(1);
assertThat(owner.getPets().get(0).getType()).isNotNull(); assertThat(owner.getPets().get(0).getType()).isNotNull();
@ -102,7 +103,7 @@ class ClinicServiceTests {
@Test @Test
@Transactional @Transactional
void shouldInsertOwner() { void shouldInsertOwner() {
Page<Owner> owners = this.owners.findByLastName("Schultz", pageable); Page<Owner> owners = this.owners.findByLastNameStartingWith("Schultz", pageable);
int found = (int) owners.getTotalElements(); int found = (int) owners.getTotalElements();
Owner owner = new Owner(); Owner owner = new Owner();
@ -114,14 +115,16 @@ class ClinicServiceTests {
this.owners.save(owner); this.owners.save(owner);
assertThat(owner.getId()).isNotZero(); assertThat(owner.getId()).isNotZero();
owners = this.owners.findByLastName("Schultz", pageable); owners = this.owners.findByLastNameStartingWith("Schultz", pageable);
assertThat(owners.getTotalElements()).isEqualTo(found + 1); assertThat(owners.getTotalElements()).isEqualTo(found + 1);
} }
@Test @Test
@Transactional @Transactional
void shouldUpdateOwner() { void shouldUpdateOwner() {
Owner owner = this.owners.findById(1); Optional<Owner> optionalOwner = this.owners.findById(1);
assertThat(optionalOwner).isPresent();
Owner owner = optionalOwner.get();
String oldLastName = owner.getLastName(); String oldLastName = owner.getLastName();
String newLastName = oldLastName + "X"; String newLastName = oldLastName + "X";
@ -129,7 +132,9 @@ class ClinicServiceTests {
this.owners.save(owner); this.owners.save(owner);
// retrieving new name from database // 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); assertThat(owner.getLastName()).isEqualTo(newLastName);
} }
@ -146,7 +151,10 @@ class ClinicServiceTests {
@Test @Test
@Transactional @Transactional
void shouldInsertPetIntoDatabaseAndGenerateId() { void shouldInsertPetIntoDatabaseAndGenerateId() {
Owner owner6 = this.owners.findById(6); Optional<Owner> optionalOwner = this.owners.findById(6);
assertThat(optionalOwner).isPresent();
Owner owner6 = optionalOwner.get();
int found = owner6.getPets().size(); int found = owner6.getPets().size();
Pet pet = new Pet(); Pet pet = new Pet();
@ -159,7 +167,9 @@ class ClinicServiceTests {
this.owners.save(owner6); 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); assertThat(owner6.getPets()).hasSize(found + 1);
// checks that id has been generated // checks that id has been generated
pet = owner6.getPet("bowser"); pet = owner6.getPet("bowser");
@ -169,7 +179,10 @@ class ClinicServiceTests {
@Test @Test
@Transactional @Transactional
void shouldUpdatePetName() { void shouldUpdatePetName() {
Owner owner6 = this.owners.findById(6); Optional<Owner> optionalOwner = this.owners.findById(6);
assertThat(optionalOwner).isPresent();
Owner owner6 = optionalOwner.get();
Pet pet7 = owner6.getPet(7); Pet pet7 = owner6.getPet(7);
String oldName = pet7.getName(); String oldName = pet7.getName();
@ -177,7 +190,9 @@ class ClinicServiceTests {
pet7.setName(newName); pet7.setName(newName);
this.owners.save(owner6); this.owners.save(owner6);
owner6 = this.owners.findById(6); optionalOwner = this.owners.findById(6);
assertThat(optionalOwner).isPresent();
owner6 = optionalOwner.get();
pet7 = owner6.getPet(7); pet7 = owner6.getPet(7);
assertThat(pet7.getName()).isEqualTo(newName); assertThat(pet7.getName()).isEqualTo(newName);
} }
@ -196,7 +211,10 @@ class ClinicServiceTests {
@Test @Test
@Transactional @Transactional
void shouldAddNewVisitForPet() { void shouldAddNewVisitForPet() {
Owner owner6 = this.owners.findById(6); Optional<Owner> optionalOwner = this.owners.findById(6);
assertThat(optionalOwner).isPresent();
Owner owner6 = optionalOwner.get();
Pet pet7 = owner6.getPet(7); Pet pet7 = owner6.getPet(7);
int found = pet7.getVisits().size(); int found = pet7.getVisits().size();
Visit visit = new Visit(); Visit visit = new Visit();
@ -205,8 +223,6 @@ class ClinicServiceTests {
owner6.addVisit(pet7.getId(), visit); owner6.addVisit(pet7.getId(), visit);
this.owners.save(owner6); this.owners.save(owner6);
owner6 = this.owners.findById(6);
assertThat(pet7.getVisits()) // assertThat(pet7.getVisits()) //
.hasSize(found + 1) // .hasSize(found + 1) //
.allMatch(value -> value.getId() != null); .allMatch(value -> value.getId() != null);
@ -214,7 +230,10 @@ class ClinicServiceTests {
@Test @Test
void shouldFindVisitsByPetId() { void shouldFindVisitsByPetId() {
Owner owner6 = this.owners.findById(6); Optional<Owner> optionalOwner = this.owners.findById(6);
assertThat(optionalOwner).isPresent();
Owner owner6 = optionalOwner.get();
Pet pet7 = owner6.getPet(7); Pet pet7 = owner6.getPet(7);
Collection<Visit> visits = pet7.getVisits(); Collection<Visit> visits = pet7.getVisits();

View file

@ -30,7 +30,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
// luck ((plain(st) UNIT test)! :) // luck ((plain(st) UNIT test)! :)
class CrashControllerTests { class CrashControllerTests {
CrashController testee = new CrashController(); final CrashController testee = new CrashController();
@Test @Test
void testTriggerException() { void testTriggerException() {

View file

@ -22,11 +22,11 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledInNativeImage; import org.junit.jupiter.api.condition.DisabledInNativeImage;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 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.PageImpl;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.context.aot.DisabledInAotMode; 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.MockMvc;
import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
@ -48,7 +48,7 @@ class VetControllerTests {
@Autowired @Autowired
private MockMvc mockMvc; private MockMvc mockMvc;
@MockBean @MockitoBean
private VetRepository vets; private VetRepository vets;
private Vet james() { private Vet james() {

View file

@ -156,8 +156,7 @@
<stringProp name="HTTPSampler.response_timeout"></stringProp> <stringProp name="HTTPSampler.response_timeout"></stringProp>
<stringProp name="HTTPSampler.protocol"></stringProp> <stringProp name="HTTPSampler.protocol"></stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp> <stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path"> <stringProp name="HTTPSampler.path">${CONTEXT_WEB}/webjars/bootstrap/dist/js/bootstrap.bundle.min.js</stringProp>
${CONTEXT_WEB}/webjars/bootstrap/5.3.3/dist/js/bootstrap.bundle.min.js</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp> <stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp> <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp> <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
@ -420,8 +419,7 @@
<stringProp name="HTTPSampler.port"></stringProp> <stringProp name="HTTPSampler.port"></stringProp>
<stringProp name="HTTPSampler.protocol"></stringProp> <stringProp name="HTTPSampler.protocol"></stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp> <stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path"> <stringProp name="HTTPSampler.path">${CONTEXT_WEB}/owners/${count}/pets/${petCount}/visits/new</stringProp>
${CONTEXT_WEB}/owners/${count}/pets/${petCount}/visits/new</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp> <stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp> <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp> <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
@ -458,8 +456,7 @@
<stringProp name="HTTPSampler.port"></stringProp> <stringProp name="HTTPSampler.port"></stringProp>
<stringProp name="HTTPSampler.protocol"></stringProp> <stringProp name="HTTPSampler.protocol"></stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp> <stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path"> <stringProp name="HTTPSampler.path">${CONTEXT_WEB}/owners/${count}/pets/${petCount}/visits/new</stringProp>
${CONTEXT_WEB}/owners/${count}/pets/${petCount}/visits/new</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp> <stringProp name="HTTPSampler.method">POST</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp> <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp> <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
@ -540,4 +537,4 @@
</hashTree> </hashTree>
</hashTree> </hashTree>
</hashTree> </hashTree>
</jmeterTestPlan> </jmeterTestPlan>