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