diff --git a/README.md b/README.md
index 1f865c56f..4a61555b5 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,57 @@ Or you can run it from Maven directly using the Spring Boot Maven plugin. If you
> NOTE: If you prefer to use Gradle, you can build the app using `./gradlew build` and look for the jar file in `build/libs`.
+## 🔧 New Features Added
+
+### ✅ PetAttribute Module
+
+- Added a new model `PetAttribute` linked to `PetType` to capture details like temperament or breed.
+- REST endpoints:
+ - `POST /api/pettypes/{petTypeId}/attributes` – Add attribute
+ - `GET /api/pettypes/{petTypeId}/attributes` – Get attributes by pet type
+
+### ⚡ gRPC Integration
+
+- Introduced gRPC support using the proto definition `pet-attribute.proto`.
+- Sample proto file:
+ ```proto
+ syntax = "proto3";
+ option java_multiple_files = true;
+ option java_package = "org.springframework.samples.petclinic.grpc";
+ option java_outer_classname = "PetAttributeProto";
+
+ service PetAttributeService {
+ rpc GetAttributes(PetAttributeRequest) returns (PetAttributeList);
+ rpc AddAttribute(NewPetAttribute) returns (PetAttributeResponse);
+ }
+
+### ⚙️ gRPC Java Classes
+
+- Auto-generated under:
+
+ target/generated-sources/grpc
+
+
+### 🔗 Spring HATEOAS Support
+
+Hypermedia links are now included in `PetAttribute` responses.
+
+**Example Response:**
+
+```json
+{
+ "temperament": "Energetic",
+ "_links": {
+ "self": {
+ "href": "http://localhost:8080/api/pettypes/1/attributes/3"
+ },
+ "petType": {
+ "href": "http://localhost:8080/api/pettypes/1"
+ }
+ }
+}
+```
+
## Building a Container
There is no `Dockerfile` in this project. You can build a container image (if you have a docker daemon) using the Spring Boot build plugin:
diff --git a/pom.xml b/pom.xml
index 8576c22ba..92cc35197 100644
--- a/pom.xml
+++ b/pom.xml
@@ -36,7 +36,7 @@
3.6.0
0.0.11
0.0.46
-
+ windows-x86_64
@@ -146,10 +146,65 @@
jakarta.xml.bind-api
+
+
+ net.devh
+ grpc-server-spring-boot-starter
+ 3.1.0.RELEASE
+
+
+
+ javax.annotation
+ javax.annotation-api
+ 1.3.2
+
+
+
+ org.springframework.boot
+ spring-boot-starter-hateoas
+
+
+
+
+
+
+ kr.motd.maven
+ os-maven-plugin
+ 1.6.2
+
+
+
+ org.xolstice.maven.plugins
+ protobuf-maven-plugin
+ 0.6.1
+
+ ${project.basedir}/src/main/proto
+ com.google.protobuf:protoc:3.21.12:exe:${os.detected.classifier}
+
+
+
+ compile
+
+ compile
+
+
+
+ compile-grpc
+
+ compile-custom
+
+
+ grpc-java
+ io.grpc:protoc-gen-grpc-java:1.58.0:exe:${os.detected.classifier}
+
+
+
+
+
org.apache.maven.plugins
maven-enforcer-plugin
diff --git a/src/main/java/org/springframework/samples/petclinic/owner/PetType.java b/src/main/java/org/springframework/samples/petclinic/owner/PetType.java
index e7d63d1aa..76097dd22 100644
--- a/src/main/java/org/springframework/samples/petclinic/owner/PetType.java
+++ b/src/main/java/org/springframework/samples/petclinic/owner/PetType.java
@@ -15,10 +15,15 @@
*/
package org.springframework.samples.petclinic.owner;
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.OneToMany;
import org.springframework.samples.petclinic.model.NamedEntity;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
+import org.springframework.samples.petclinic.pet.model.PetAttribute;
+
+import java.util.Set;
/**
* @author Juergen Hoeller Can be Cat, Dog, Hamster...
@@ -27,4 +32,7 @@ import jakarta.persistence.Table;
@Table(name = "types")
public class PetType extends NamedEntity {
+ @OneToMany(mappedBy = "petType", cascade = CascadeType.ALL)
+ private Set attributes;
+
}
diff --git a/src/main/java/org/springframework/samples/petclinic/pet/controller/PetAttributeController.java b/src/main/java/org/springframework/samples/petclinic/pet/controller/PetAttributeController.java
new file mode 100644
index 000000000..c027e4660
--- /dev/null
+++ b/src/main/java/org/springframework/samples/petclinic/pet/controller/PetAttributeController.java
@@ -0,0 +1,66 @@
+package org.springframework.samples.petclinic.pet.controller;
+
+import org.springframework.hateoas.CollectionModel;
+import org.springframework.hateoas.EntityModel;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.samples.petclinic.owner.PetType;
+import org.springframework.samples.petclinic.pet.model.PetAttribute;
+import org.springframework.samples.petclinic.pet.service.PetAttributeService;
+import org.springframework.samples.petclinic.pet.service.PetTypeService;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
+import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
+
+/**
+ * @author Rohit Lalwani
+ */
+@RestController
+@RequestMapping("/api/petTypes/{typeId}/attributes")
+public class PetAttributeController {
+
+ private final PetTypeService petTypeService;
+
+ private final PetAttributeService petAttributeService;
+
+ private final PetAttributeModelAssembler assembler;
+
+ public PetAttributeController(PetTypeService petTypeService, PetAttributeService attrService,
+ PetAttributeModelAssembler assembler) {
+ this.petTypeService = petTypeService;
+ this.petAttributeService = attrService;
+ this.assembler = assembler;
+ }
+
+ @PostMapping
+ public ResponseEntity> createAttribute(@PathVariable Integer typeId,
+ @RequestBody PetAttribute attr) {
+ PetType type = petTypeService.findPetTypeById(typeId);
+ if (type == null)
+ return ResponseEntity.notFound().build();
+
+ attr.setPetType(type);
+ PetAttribute saved = petAttributeService.save(attr);
+
+ return ResponseEntity.status(HttpStatus.CREATED).body(assembler.toModel(saved));
+ }
+
+ @GetMapping
+ public ResponseEntity>> getAttributes(@PathVariable Integer typeId) {
+ List attrs = petAttributeService.findByPetTypeId(typeId);
+
+ List> attrModels = attrs.stream().map(assembler::toModel).toList();
+
+ return ResponseEntity.ok(CollectionModel.of(attrModels,
+ linkTo(methodOn(PetAttributeController.class).getAttributes(typeId)).withSelfRel()));
+ }
+
+}
diff --git a/src/main/java/org/springframework/samples/petclinic/pet/controller/PetAttributeModelAssembler.java b/src/main/java/org/springframework/samples/petclinic/pet/controller/PetAttributeModelAssembler.java
new file mode 100644
index 000000000..a1713af14
--- /dev/null
+++ b/src/main/java/org/springframework/samples/petclinic/pet/controller/PetAttributeModelAssembler.java
@@ -0,0 +1,27 @@
+package org.springframework.samples.petclinic.pet.controller;
+
+import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
+import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
+
+import org.springframework.hateoas.EntityModel;
+import org.springframework.hateoas.server.RepresentationModelAssembler;
+import org.springframework.samples.petclinic.pet.model.PetAttribute;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author Rohit Lalwani
+ */
+@Component
+public class PetAttributeModelAssembler
+ implements RepresentationModelAssembler> {
+
+ @Override
+ public EntityModel toModel(PetAttribute attr) {
+ Integer typeId = attr.getPetType().getId();
+ return EntityModel.of(attr,
+ linkTo(methodOn(PetAttributeController.class).getAttributes(typeId)).withRel("allAttributes"),
+ linkTo(methodOn(PetAttributeController.class).createAttribute(typeId, attr))
+ .withRel("createAttribute"));
+ }
+
+}
diff --git a/src/main/java/org/springframework/samples/petclinic/pet/grpc/PetAttributeGrpcService.java b/src/main/java/org/springframework/samples/petclinic/pet/grpc/PetAttributeGrpcService.java
new file mode 100644
index 000000000..c76b76469
--- /dev/null
+++ b/src/main/java/org/springframework/samples/petclinic/pet/grpc/PetAttributeGrpcService.java
@@ -0,0 +1,76 @@
+package org.springframework.samples.petclinic.pet.grpc;
+
+import io.grpc.stub.StreamObserver;
+import net.devh.boot.grpc.server.service.GrpcService;
+import org.springframework.samples.petclinic.grpc.NewPetAttribute;
+import org.springframework.samples.petclinic.grpc.PetAttributeList;
+import org.springframework.samples.petclinic.grpc.PetAttributeRequest;
+import org.springframework.samples.petclinic.grpc.PetAttributeResponse;
+import org.springframework.samples.petclinic.grpc.PetAttributeServiceGrpc;
+import org.springframework.samples.petclinic.owner.PetType;
+import org.springframework.samples.petclinic.pet.model.PetAttribute;
+import org.springframework.samples.petclinic.pet.service.PetAttributeService;
+import org.springframework.samples.petclinic.pet.service.PetTypeService;
+
+import java.util.List;
+
+/**
+ * @author Rohit Lalwani
+ */
+@GrpcService
+public class PetAttributeGrpcService extends PetAttributeServiceGrpc.PetAttributeServiceImplBase {
+
+ private final PetAttributeService petAttributeService;
+
+ private final PetTypeService petTypeService;
+
+ public PetAttributeGrpcService(PetAttributeService petAttributeService, PetTypeService petTypeService) {
+ this.petAttributeService = petAttributeService;
+ this.petTypeService = petTypeService;
+ }
+
+ public void GetAttributes(PetAttributeRequest request, StreamObserver responseObserver) {
+ List attributes = petAttributeService.findByPetTypeId(request.getTypeId());
+
+ PetAttributeList.Builder listBuilder = PetAttributeList.newBuilder();
+ for (PetAttribute attr : attributes) {
+ PetAttributeResponse response = PetAttributeResponse.newBuilder()
+ .setId(attr.getId())
+ .setTemperament(attr.getTemperament())
+ .setWeight(attr.getWeight())
+ .setLength(attr.getLength())
+ .build();
+ listBuilder.addAttributes(response);
+ }
+
+ responseObserver.onNext(listBuilder.build());
+ responseObserver.onCompleted();
+ }
+
+ public void AddAttribute(NewPetAttribute request, StreamObserver responseObserver) {
+ PetType petType = petTypeService.findPetTypeById(request.getTypeId());
+ if (petType == null) {
+ responseObserver.onError(new IllegalArgumentException("Pet type not found"));
+ return;
+ }
+
+ PetAttribute attr = new PetAttribute();
+ attr.setTemperament(request.getTemperament());
+ attr.setWeight(request.getWeight());
+ attr.setLength(request.getLength());
+ attr.setPetType(petType);
+
+ PetAttribute saved = petAttributeService.save(attr);
+
+ PetAttributeResponse response = PetAttributeResponse.newBuilder()
+ .setId(saved.getId())
+ .setTemperament(saved.getTemperament())
+ .setWeight(saved.getWeight())
+ .setLength(saved.getLength())
+ .build();
+
+ responseObserver.onNext(response);
+ responseObserver.onCompleted();
+ }
+
+}
diff --git a/src/main/java/org/springframework/samples/petclinic/pet/model/PetAttribute.java b/src/main/java/org/springframework/samples/petclinic/pet/model/PetAttribute.java
new file mode 100644
index 000000000..786350e50
--- /dev/null
+++ b/src/main/java/org/springframework/samples/petclinic/pet/model/PetAttribute.java
@@ -0,0 +1,78 @@
+package org.springframework.samples.petclinic.pet.model;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.validation.constraints.NotNull;
+import org.springframework.samples.petclinic.owner.PetType;
+
+import java.io.Serializable;
+
+/**
+ * @author Rohit Lalwani
+ */
+@Entity
+public class PetAttribute implements Serializable {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Integer id;
+
+ @NotNull
+ private String temperament;
+
+ @NotNull
+ private Double weight;
+
+ @NotNull
+ private Double length;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "type_id")
+ private PetType petType;
+
+ public Integer getId() {
+ return id;
+ }
+
+ public void setId(Integer id) {
+ this.id = id;
+ }
+
+ public String getTemperament() {
+ return temperament;
+ }
+
+ public void setTemperament(String temperament) {
+ this.temperament = temperament;
+ }
+
+ public Double getWeight() {
+ return weight;
+ }
+
+ public void setWeight(Double weight) {
+ this.weight = weight;
+ }
+
+ public Double getLength() {
+ return length;
+ }
+
+ public void setLength(Double length) {
+ this.length = length;
+ }
+
+ public PetType getPetType() {
+ return petType;
+ }
+
+ public void setPetType(PetType petType) {
+ this.petType = petType;
+ }
+
+}
diff --git a/src/main/java/org/springframework/samples/petclinic/pet/repository/PetAttributeRepository.java b/src/main/java/org/springframework/samples/petclinic/pet/repository/PetAttributeRepository.java
new file mode 100644
index 000000000..b37bf58e0
--- /dev/null
+++ b/src/main/java/org/springframework/samples/petclinic/pet/repository/PetAttributeRepository.java
@@ -0,0 +1,17 @@
+package org.springframework.samples.petclinic.pet.repository;
+
+import org.springframework.data.jpa.repository.EntityGraph;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.samples.petclinic.pet.model.PetAttribute;
+
+import java.util.List;
+
+/**
+ * @author Rohit Lalwani
+ */
+public interface PetAttributeRepository extends CrudRepository {
+
+ @EntityGraph(attributePaths = "petType")
+ List findByPetTypeId(Integer typeId);
+
+}
diff --git a/src/main/java/org/springframework/samples/petclinic/pet/service/PetAttributeService.java b/src/main/java/org/springframework/samples/petclinic/pet/service/PetAttributeService.java
new file mode 100644
index 000000000..61ef5a7a0
--- /dev/null
+++ b/src/main/java/org/springframework/samples/petclinic/pet/service/PetAttributeService.java
@@ -0,0 +1,29 @@
+package org.springframework.samples.petclinic.pet.service;
+
+import org.springframework.samples.petclinic.pet.model.PetAttribute;
+import org.springframework.samples.petclinic.pet.repository.PetAttributeRepository;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * @author Rohit Lalwani
+ */
+@Service
+public class PetAttributeService {
+
+ private final PetAttributeRepository petAttributeRepository;
+
+ public PetAttributeService(PetAttributeRepository repo) {
+ this.petAttributeRepository = repo;
+ }
+
+ public PetAttribute save(PetAttribute attr) {
+ return petAttributeRepository.save(attr);
+ }
+
+ public List findByPetTypeId(Integer petTypeId) {
+ return petAttributeRepository.findByPetTypeId(petTypeId);
+ }
+
+}
diff --git a/src/main/java/org/springframework/samples/petclinic/pet/service/PetTypeService.java b/src/main/java/org/springframework/samples/petclinic/pet/service/PetTypeService.java
new file mode 100644
index 000000000..69676d447
--- /dev/null
+++ b/src/main/java/org/springframework/samples/petclinic/pet/service/PetTypeService.java
@@ -0,0 +1,26 @@
+package org.springframework.samples.petclinic.pet.service;
+
+import org.springframework.samples.petclinic.owner.PetType;
+import org.springframework.samples.petclinic.owner.PetTypeRepository;
+import org.springframework.stereotype.Service;
+
+import java.util.Optional;
+
+/**
+ * @author Rohit Lalwani
+ */
+@Service
+public class PetTypeService {
+
+ private final PetTypeRepository petTypeRepository;
+
+ public PetTypeService(PetTypeRepository petTypeRepository) {
+ this.petTypeRepository = petTypeRepository;
+ }
+
+ public PetType findPetTypeById(Integer id) {
+ Optional petType = petTypeRepository.findById(id);
+ return petType.orElse(null);
+ }
+
+}
diff --git a/src/main/proto/petattribute.proto b/src/main/proto/petattribute.proto
new file mode 100644
index 000000000..39802882c
--- /dev/null
+++ b/src/main/proto/petattribute.proto
@@ -0,0 +1,32 @@
+syntax = "proto3";
+
+option java_multiple_files = true;
+option java_package = "org.springframework.samples.petclinic.grpc";
+option java_outer_classname = "PetAttributeProto";
+
+message PetAttributeRequest {
+ int32 typeId = 1;
+}
+
+message NewPetAttribute {
+ int32 typeId = 1;
+ string temperament = 2;
+ double weight = 3;
+ double length = 4;
+}
+
+message PetAttributeResponse {
+ int32 id = 1;
+ string temperament = 2;
+ double weight = 3;
+ double length = 4;
+}
+
+message PetAttributeList {
+ repeated PetAttributeResponse attributes = 1;
+}
+
+service PetAttributeService {
+ rpc GetAttributes (PetAttributeRequest) returns (PetAttributeList);
+ rpc AddAttribute (NewPetAttribute) returns (PetAttributeResponse);
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 6ed985654..1c1eedc83 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -23,3 +23,5 @@ logging.level.org.springframework=INFO
# Maximum time static resources should be cached
spring.web.resources.cache.cachecontrol.max-age=12h
+
+grpc.server.port=9090
diff --git a/src/main/resources/db/h2/schema.sql b/src/main/resources/db/h2/schema.sql
index 4a6c322cb..9a01f5424 100644
--- a/src/main/resources/db/h2/schema.sql
+++ b/src/main/resources/db/h2/schema.sql
@@ -62,3 +62,12 @@ CREATE TABLE visits (
);
ALTER TABLE visits ADD CONSTRAINT fk_visits_pets FOREIGN KEY (pet_id) REFERENCES pets (id);
CREATE INDEX visits_pet_id ON visits (pet_id);
+
+CREATE TABLE IF NOT EXISTS pet_attribute (
+ id INT PRIMARY KEY AUTO_INCREMENT,
+ temperament VARCHAR(50) NOT NULL,
+ weight DOUBLE NOT NULL,
+ length DOUBLE NOT NULL,
+ type_id INT,
+ FOREIGN KEY (type_id) REFERENCES types(id)
+);
diff --git a/src/main/resources/db/hsqldb/schema.sql b/src/main/resources/db/hsqldb/schema.sql
index 5d6760a4b..3cbab341f 100644
--- a/src/main/resources/db/hsqldb/schema.sql
+++ b/src/main/resources/db/hsqldb/schema.sql
@@ -62,3 +62,12 @@ CREATE TABLE visits (
);
ALTER TABLE visits ADD CONSTRAINT fk_visits_pets FOREIGN KEY (pet_id) REFERENCES pets (id);
CREATE INDEX visits_pet_id ON visits (pet_id);
+
+CREATE TABLE IF NOT EXISTS pet_attribute (
+ id INT PRIMARY KEY AUTO_INCREMENT,
+ temperament VARCHAR(50) NOT NULL,
+ weight DOUBLE NOT NULL,
+ length DOUBLE NOT NULL,
+ type_id INT,
+ FOREIGN KEY (type_id) REFERENCES types(id)
+);
diff --git a/src/main/resources/db/mysql/schema.sql b/src/main/resources/db/mysql/schema.sql
index 2591a516d..7497b0edf 100644
--- a/src/main/resources/db/mysql/schema.sql
+++ b/src/main/resources/db/mysql/schema.sql
@@ -53,3 +53,12 @@ CREATE TABLE IF NOT EXISTS visits (
description VARCHAR(255),
FOREIGN KEY (pet_id) REFERENCES pets(id)
) engine=InnoDB;
+
+CREATE TABLE IF NOT EXISTS pet_attribute (
+ id INT PRIMARY KEY AUTO_INCREMENT,
+ temperament VARCHAR(50) NOT NULL,
+ weight DOUBLE NOT NULL,
+ length DOUBLE NOT NULL,
+ type_id INT,
+ FOREIGN KEY (type_id) REFERENCES types(id)
+) engine=InnoDB;
diff --git a/src/main/resources/db/postgres/schema.sql b/src/main/resources/db/postgres/schema.sql
index 1bd582dc2..827669134 100644
--- a/src/main/resources/db/postgres/schema.sql
+++ b/src/main/resources/db/postgres/schema.sql
@@ -50,3 +50,12 @@ CREATE TABLE IF NOT EXISTS visits (
description TEXT
);
CREATE INDEX ON visits (pet_id);
+
+CREATE TABLE IF NOT EXISTS pet_attribute (
+ id INT PRIMARY KEY AUTO_INCREMENT,
+ temperament VARCHAR(50) NOT NULL,
+ weight DOUBLE NOT NULL,
+ length DOUBLE NOT NULL,
+ type_id INT,
+ FOREIGN KEY (type_id) REFERENCES types(id)
+);
diff --git a/src/test/java/org/springframework/samples/petclinic/pet/controller/PetAttributeControllerTest.java b/src/test/java/org/springframework/samples/petclinic/pet/controller/PetAttributeControllerTest.java
new file mode 100644
index 000000000..8a782b210
--- /dev/null
+++ b/src/test/java/org/springframework/samples/petclinic/pet/controller/PetAttributeControllerTest.java
@@ -0,0 +1,89 @@
+package org.springframework.samples.petclinic.pet.controller;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.hateoas.CollectionModel;
+import org.springframework.hateoas.EntityModel;
+import org.springframework.http.ResponseEntity;
+import org.springframework.samples.petclinic.owner.PetType;
+import org.springframework.samples.petclinic.pet.model.PetAttribute;
+import org.springframework.samples.petclinic.pet.service.PetAttributeService;
+import org.springframework.samples.petclinic.pet.service.PetTypeService;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class PetAttributeControllerTest {
+
+ @Mock
+ private PetTypeService petTypeService;
+
+ @Mock
+ private PetAttributeService petAttributeService;
+
+ @Mock
+ private PetAttributeModelAssembler assembler;
+
+ @InjectMocks
+ private PetAttributeController controller;
+
+ @Test
+ void testCreateAttribute_returnsCreated() {
+ PetType type = new PetType();
+ PetAttribute attr = new PetAttribute();
+ attr.setTemperament("Energetic");
+
+ when(petTypeService.findPetTypeById(1)).thenReturn(type);
+ when(petAttributeService.save(attr)).thenReturn(attr);
+ when(assembler.toModel(attr)).thenReturn(EntityModel.of(attr));
+
+ ResponseEntity> response = controller.createAttribute(1, attr);
+
+ assertEquals(201, response.getStatusCode().value());
+ assertNotNull(response.getBody());
+ assertEquals("Energetic", Objects.requireNonNull(response.getBody().getContent()).getTemperament());
+ verify(petAttributeService).save(attr);
+ }
+
+ @Test
+ void testCreateAttribute_petTypeNotFound() {
+ PetAttribute attr = new PetAttribute();
+
+ when(petTypeService.findPetTypeById(999)).thenReturn(null);
+
+ ResponseEntity> response = controller.createAttribute(999, attr);
+
+ assertEquals(404, response.getStatusCode().value());
+ assertNull(response.getBody());
+ }
+
+ @Test
+ void testGetAttributes_returnsList() {
+ PetAttribute attr = new PetAttribute();
+ attr.setTemperament("Curious");
+
+ List attributes = Collections.singletonList(attr);
+ EntityModel model = EntityModel.of(attr);
+
+ when(petAttributeService.findByPetTypeId(1)).thenReturn(attributes);
+ when(assembler.toModel(attr)).thenReturn(model);
+
+ ResponseEntity>> response = controller.getAttributes(1);
+
+ assertEquals(200, response.getStatusCode().value());
+ assertNotNull(response.getBody());
+ assertEquals(1, response.getBody().getContent().size());
+ }
+
+}
diff --git a/src/test/java/org/springframework/samples/petclinic/pet/service/PetAttributeServiceTest.java b/src/test/java/org/springframework/samples/petclinic/pet/service/PetAttributeServiceTest.java
new file mode 100644
index 000000000..875a0e4a4
--- /dev/null
+++ b/src/test/java/org/springframework/samples/petclinic/pet/service/PetAttributeServiceTest.java
@@ -0,0 +1,53 @@
+package org.springframework.samples.petclinic.pet.service;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.samples.petclinic.pet.model.PetAttribute;
+import org.springframework.samples.petclinic.pet.repository.PetAttributeRepository;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class PetAttributeServiceTest {
+
+ @Mock
+ private PetAttributeRepository repository;
+
+ @InjectMocks
+ private PetAttributeService service;
+
+ @Test
+ void testSave() {
+ PetAttribute attr = new PetAttribute();
+ attr.setTemperament("Calm");
+ attr.setWeight(10.0);
+ attr.setLength(20.0);
+
+ when(repository.save(attr)).thenReturn(attr);
+
+ PetAttribute saved = service.save(attr);
+ assertEquals("Calm", saved.getTemperament());
+ verify(repository, times(1)).save(attr);
+ }
+
+ @Test
+ void testFindByPetTypeId() {
+ PetAttribute attr = new PetAttribute();
+ attr.setTemperament("Playful");
+ when(repository.findByPetTypeId(1)).thenReturn(Arrays.asList(attr));
+
+ List result = service.findByPetTypeId(1);
+ assertEquals(1, result.size());
+ verify(repository, times(1)).findByPetTypeId(1);
+ }
+
+}
diff --git a/src/test/java/org/springframework/samples/petclinic/pet/service/PetTypeServiceTest.java b/src/test/java/org/springframework/samples/petclinic/pet/service/PetTypeServiceTest.java
new file mode 100644
index 000000000..57343f215
--- /dev/null
+++ b/src/test/java/org/springframework/samples/petclinic/pet/service/PetTypeServiceTest.java
@@ -0,0 +1,46 @@
+package org.springframework.samples.petclinic.pet.service;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.samples.petclinic.owner.PetType;
+import org.springframework.samples.petclinic.owner.PetTypeRepository;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class PetTypeServiceTest {
+
+ @Mock
+ private PetTypeRepository petTypeRepository;
+
+ @InjectMocks
+ private PetTypeService petTypeService;
+
+ @Test
+ void testFindPetTypeById_found() {
+ PetType type = new PetType();
+ type.setId(1);
+ when(petTypeRepository.findById(1)).thenReturn(Optional.of(type));
+
+ PetType found = petTypeService.findPetTypeById(1);
+ assertNotNull(found);
+ assertEquals(found.getId(), Optional.of(1).get());
+ }
+
+ @Test
+ void testFindPetTypeById_notFound() {
+ when(petTypeRepository.findById(2)).thenReturn(Optional.empty());
+ PetType found = petTypeService.findPetTypeById(2);
+ assertNull(found);
+ }
+
+}
diff --git a/src/test/java/org/springframework/samples/petclinic/service/EntityUtils.java b/src/test/java/org/springframework/samples/petclinic/service/EntityUtils.java
index 180ef07f1..1fea6d9a8 100644
--- a/src/test/java/org/springframework/samples/petclinic/service/EntityUtils.java
+++ b/src/test/java/org/springframework/samples/petclinic/service/EntityUtils.java
@@ -27,7 +27,7 @@ import java.util.Collection;
*
* @author Juergen Hoeller
* @author Sam Brannen
- * @see org.springframework.samples.petclinic.model.BaseEntity
+ * @see BaseEntity
* @since 29.10.2003
*/
public abstract class EntityUtils {
diff --git a/src/test/java/org/springframework/samples/petclinic/system/CrashControllerIntegrationTests.java b/src/test/java/org/springframework/samples/petclinic/system/CrashControllerIntegrationTests.java
index ed8b0819a..8ab5cacfe 100644
--- a/src/test/java/org/springframework/samples/petclinic/system/CrashControllerIntegrationTests.java
+++ b/src/test/java/org/springframework/samples/petclinic/system/CrashControllerIntegrationTests.java
@@ -46,8 +46,8 @@ import org.springframework.http.ResponseEntity;
* @author Alex Lutz
*/
// NOT Waiting https://github.com/spring-projects/spring-boot/issues/5574
-@SpringBootTest(webEnvironment = RANDOM_PORT,
- properties = { "server.error.include-message=ALWAYS", "management.endpoints.enabled-by-default=false" })
+@SpringBootTest(webEnvironment = RANDOM_PORT, properties = { "server.error.include-message=ALWAYS",
+ "management.endpoints.enabled-by-default=false", "grpc.server.port=0", "grpc.server.address=127.0.0.1" })
class CrashControllerIntegrationTests {
@Value(value = "${local.server.port}")