From c9ff51d9b2fe067b1ebf363dd9b491a54326b14a Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Wed, 8 Apr 2026 19:49:55 +0200 Subject: [PATCH 01/85] test: add null/default value checks for User entity --- .../vet1177/entities/UserEntityTest.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/test/java/org/example/vet1177/entities/UserEntityTest.java diff --git a/src/test/java/org/example/vet1177/entities/UserEntityTest.java b/src/test/java/org/example/vet1177/entities/UserEntityTest.java new file mode 100644 index 00000000..875ad5ec --- /dev/null +++ b/src/test/java/org/example/vet1177/entities/UserEntityTest.java @@ -0,0 +1,45 @@ +package org.example.vet1177.entities; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class UserEntityTest { + private User user; + + @BeforeEach + void setUp() { + user = new User("Frida Svensson", "Frida@example.se", "lösenord!", Role.OWNER); + } + + //kontrollerar att fält är null (eller true för isActive) + + @Test + void getId_shouldBeNullBeforePersist() { + assertThat(user.getId()).isNull(); + } + + @Test + void isActive_shouldBeTrueByDefault() { + assertThat(user.isActive()).isTrue(); + } + + @Test + void getClinic_shouldBeNullWhenNotSet() { + assertThat(user.getClinic()).isNull(); + } + + @Test + void getCreatedAt_shouldBeNullBeforeOnCreate() { + assertThat(user.getCreatedAt()).isNull(); + } + + @Test + void getUpdatedAt_shouldBeNullBeforeOnCreate() { + assertThat(user.getUpdatedAt()).isNull(); + } + + + +} From 5420a7786e89c28d1d75770dbe9b3449aee8cc5c Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Wed, 8 Apr 2026 20:21:36 +0200 Subject: [PATCH 02/85] test: add lifecycle callback tests for User entity --- .../vet1177/entities/UserEntityTest.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/test/java/org/example/vet1177/entities/UserEntityTest.java b/src/test/java/org/example/vet1177/entities/UserEntityTest.java index 875ad5ec..3a8bb4c1 100644 --- a/src/test/java/org/example/vet1177/entities/UserEntityTest.java +++ b/src/test/java/org/example/vet1177/entities/UserEntityTest.java @@ -3,6 +3,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.time.Instant; + import static org.assertj.core.api.AssertionsForClassTypes.assertThat; public class UserEntityTest { @@ -40,6 +42,43 @@ void getUpdatedAt_shouldBeNullBeforeOnCreate() { assertThat(user.getUpdatedAt()).isNull(); } + //livscykel: onCreate + @Test + void onCreate_shouldSetBothTimestampsToNonNull() { + user.onCreate(); + assertThat(user.getCreatedAt()).isNotNull(); + assertThat(user.getUpdatedAt()).isNotNull(); + } + + @Test + void onCreate_shouldSetTimestampsCloseToNow() { + Instant before = Instant.now(); + user.onCreate(); + Instant after = Instant.now(); + + assertThat(user.getCreatedAt()).isBetween(before, after); + assertThat(user.getUpdatedAt()).isBetween(before, after); + } + + //livscykel på onUpdate + @Test + void onUpdate_shouldRefreshUpdatedAt() { + user.onCreate(); + + user.onUpdate(); + + assertThat(user.getUpdatedAt()).isNotNull(); + } + + @Test + void onUpdate_shouldNotModifyCreatedAt() { + user.onCreate(); + Instant originalCreatedAt = user.getCreatedAt(); + + user.onUpdate(); + + assertThat(user.getCreatedAt()).isEqualTo(originalCreatedAt); + } } From 3b80e90facc37ddbcd16eaca53de7dab9bc0db11 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Wed, 8 Apr 2026 20:41:51 +0200 Subject: [PATCH 03/85] test: add UserDetails tests --- .../vet1177/entities/UserEntityTest.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/test/java/org/example/vet1177/entities/UserEntityTest.java b/src/test/java/org/example/vet1177/entities/UserEntityTest.java index 3a8bb4c1..b4f094f6 100644 --- a/src/test/java/org/example/vet1177/entities/UserEntityTest.java +++ b/src/test/java/org/example/vet1177/entities/UserEntityTest.java @@ -80,5 +80,41 @@ void onUpdate_shouldNotModifyCreatedAt() { assertThat(user.getCreatedAt()).isEqualTo(originalCreatedAt); } + //UserDetails + + @Test + void getUsername_shouldReturnEmail() { + assertThat(user.getUsername()).isEqualTo("Frida@example.se"); + } + + @Test + void getPassword_shouldReturnPasswordHash() { + assertThat(user.getPassword()).isEqualTo("lösenord!"); + } + + @Test + void isEnabled_shouldReturnTrueWhenActive() { + assertThat(user.isEnabled()).isTrue(); + } + + @Test + void isEnabled_shouldReturnFalseWhenInactive() { + user.setActive(false); + + assertThat(user.isEnabled()).isFalse(); + } + + @Test + void isAccountNonLocked_shouldReturnTrueWhenActive() { + assertThat(user.isAccountNonLocked()).isTrue(); + } + + @Test + void isAccountNonLocked_shouldReturnFalseWhenInactive() { + user.setActive(false); + + assertThat(user.isAccountNonLocked()).isFalse(); + } + } From 8cebc4d7300c32e0e86a1b85b5a321bfa81f8380 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Wed, 8 Apr 2026 21:08:49 +0200 Subject: [PATCH 04/85] test: add getAuthorties test --- .../vet1177/entities/UserEntityTest.java | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/example/vet1177/entities/UserEntityTest.java b/src/test/java/org/example/vet1177/entities/UserEntityTest.java index b4f094f6..657333f3 100644 --- a/src/test/java/org/example/vet1177/entities/UserEntityTest.java +++ b/src/test/java/org/example/vet1177/entities/UserEntityTest.java @@ -2,10 +2,14 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.security.core.GrantedAuthority; import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; public class UserEntityTest { private User user; @@ -116,5 +120,35 @@ void isAccountNonLocked_shouldReturnFalseWhenInactive() { assertThat(user.isAccountNonLocked()).isFalse(); } + //GetAuthorties + @Test + void getAuthorities_ownerRole_shouldReturnRoleOwner() { + User owner = new User("Anna", "anna@mail.se", "hash", Role.OWNER); + Collection authorities = owner.getAuthorities(); + + assertThat(new ArrayList<>(authorities)) + .extracting(a -> a.getAuthority()) + .containsExactly("ROLE_OWNER"); + } + + @Test + void getAuthorities_vetRole_shouldReturnRoleVet() { + User vet = new User("Dr. Erik", "erik@vet.se", "hash", Role.VET); + Collection authorities = vet.getAuthorities(); + + assertThat(new ArrayList<>(authorities)) + .extracting(a -> a.getAuthority()) + .containsExactly("ROLE_VET"); + } + + @Test + void getAuthorities_adminRole_shouldReturnRoleAdmin() { + User admin = new User("Admin", "admin@vet.se", "hash", Role.ADMIN); + Collection authorities = admin.getAuthorities(); + + assertThat(new ArrayList<>(authorities)) + .extracting(a -> a.getAuthority()) + .containsExactly("ROLE_ADMIN"); + } } From 9414973555c8389a56ef08f4db7551730862dfa5 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Wed, 8 Apr 2026 22:12:39 +0200 Subject: [PATCH 05/85] test: addAttachment and removeAttachment --- .../vet1177/entities/UserEntityTest.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/test/java/org/example/vet1177/entities/UserEntityTest.java b/src/test/java/org/example/vet1177/entities/UserEntityTest.java index 657333f3..aaa6df77 100644 --- a/src/test/java/org/example/vet1177/entities/UserEntityTest.java +++ b/src/test/java/org/example/vet1177/entities/UserEntityTest.java @@ -151,4 +151,63 @@ void getAuthorities_adminRole_shouldReturnRoleAdmin() { .containsExactly("ROLE_ADMIN"); } + //addAttachment + + @Test + void addAttachment_shouldAddToList() { + Attachment attachment = new Attachment(); + + user.addAttachment(attachment); + + assertThat(user.getUploadedAttachments()).contains(attachment); + } + + @Test + void addAttachment_shouldSetUploadedByOnAttachment() { + Attachment attachment = new Attachment(); + + user.addAttachment(attachment); + + assertThat(attachment.getUploadedBy()).isSameAs(user); + } + + @Test + void addAttachment_nullAttachment_shouldNotAddToList() { + user.addAttachment(null); + + assertThat(user.getUploadedAttachments()).isEmpty(); + } + + // removeAttachment + + @Test + void removeAttachment_shouldRemoveFromList() { + Attachment attachment = new Attachment(); + user.addAttachment(attachment); + + user.removeAttachment(attachment); + + assertThat(user.getUploadedAttachments()).doesNotContain(attachment); + } + + @Test + void removeAttachment_shouldSetUploadedByToNull() { + Attachment attachment = new Attachment(); + user.addAttachment(attachment); + + user.removeAttachment(attachment); + + assertThat(attachment.getUploadedBy()).isNull(); + } + + @Test + void removeAttachment_nullAttachment_shouldNotThrow() { + user.addAttachment(new Attachment()); + + user.removeAttachment(null); + + assertThat(user.getUploadedAttachments()).hasSize(1); + } + + } From 575477d14f21ef41db39d0f4d2d8af44d29a0fbf Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Wed, 8 Apr 2026 22:30:06 +0200 Subject: [PATCH 06/85] test: getUploadedAttachments --- .../example/vet1177/entities/UserEntityTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/java/org/example/vet1177/entities/UserEntityTest.java b/src/test/java/org/example/vet1177/entities/UserEntityTest.java index aaa6df77..310c4a34 100644 --- a/src/test/java/org/example/vet1177/entities/UserEntityTest.java +++ b/src/test/java/org/example/vet1177/entities/UserEntityTest.java @@ -208,6 +208,20 @@ void removeAttachment_nullAttachment_shouldNotThrow() { assertThat(user.getUploadedAttachments()).hasSize(1); } + // getUploadedAttachments + @Test + void getUploadedAttachments_shouldReturnUnmodifiableList() { + Attachment attachment = new Attachment(); + user.addAttachment(attachment); + + assertThatThrownBy(() -> user.getUploadedAttachments().add(new Attachment())) + .isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void getUploadedAttachments_shouldBeEmptyByDefault() { + assertThat(user.getUploadedAttachments()).isEmpty(); + } } From 796e27c865972c755e5ee881b1f9749c73528d79 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Wed, 8 Apr 2026 22:31:44 +0200 Subject: [PATCH 07/85] test: getUploadedAttachments --- src/test/java/org/example/vet1177/entities/UserEntityTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/org/example/vet1177/entities/UserEntityTest.java b/src/test/java/org/example/vet1177/entities/UserEntityTest.java index 310c4a34..11eae9ef 100644 --- a/src/test/java/org/example/vet1177/entities/UserEntityTest.java +++ b/src/test/java/org/example/vet1177/entities/UserEntityTest.java @@ -223,5 +223,4 @@ void getUploadedAttachments_shouldReturnUnmodifiableList() { void getUploadedAttachments_shouldBeEmptyByDefault() { assertThat(user.getUploadedAttachments()).isEmpty(); } - } From 056713c70974786e6bc9c2f62a1e1502cf9aebdf Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Thu, 9 Apr 2026 10:28:30 +0200 Subject: [PATCH 08/85] fix: rabbit comment --- src/test/java/org/example/vet1177/entities/UserEntityTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/example/vet1177/entities/UserEntityTest.java b/src/test/java/org/example/vet1177/entities/UserEntityTest.java index 11eae9ef..81b41442 100644 --- a/src/test/java/org/example/vet1177/entities/UserEntityTest.java +++ b/src/test/java/org/example/vet1177/entities/UserEntityTest.java @@ -69,10 +69,11 @@ void onCreate_shouldSetTimestampsCloseToNow() { @Test void onUpdate_shouldRefreshUpdatedAt() { user.onCreate(); + Instant beforeUpdate = user.getUpdatedAt(); user.onUpdate(); - assertThat(user.getUpdatedAt()).isNotNull(); + assertThat(user.getUpdatedAt()).isNotNull().isAfterOrEqualTo(beforeUpdate); } @Test From 7f27cb3bd609f2280e0a6965a1d8b458e6144c4c Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Thu, 9 Apr 2026 11:27:43 +0200 Subject: [PATCH 09/85] test: test setUp and test on createUser --- .../vet1177/services/UserServiceTest.java | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 src/test/java/org/example/vet1177/services/UserServiceTest.java diff --git a/src/test/java/org/example/vet1177/services/UserServiceTest.java b/src/test/java/org/example/vet1177/services/UserServiceTest.java new file mode 100644 index 00000000..bdab2658 --- /dev/null +++ b/src/test/java/org/example/vet1177/services/UserServiceTest.java @@ -0,0 +1,188 @@ +package org.example.vet1177.services; + +import org.example.vet1177.dto.request.user.UserRequest; +import org.example.vet1177.dto.response.user.UserResponse; +import org.example.vet1177.entities.Clinic; +import org.example.vet1177.entities.Role; +import org.example.vet1177.entities.User; +import org.example.vet1177.exception.BusinessRuleException; +import org.example.vet1177.exception.ResourceNotFoundException; +import org.example.vet1177.repository.ClinicRepository; +import org.example.vet1177.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +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.dao.DataIntegrityViolationException; + +import java.lang.reflect.Field; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class UserServiceTest { + + @Mock + private UserRepository userRepository; + + @Mock + private ClinicRepository clinicRepository; + + @Mock + private PetService petService; + + @Mock + private MedicalRecordService medicalRecordService; + + @InjectMocks + private UserService userService; + + private UUID userId; + private UUID clinicId; + private User ownerUser; + private User vetUser; + private User adminUser; + private Clinic clinic; + + @BeforeEach + void setUp() throws Exception { + userId = UUID.randomUUID(); + clinicId = UUID.randomUUID(); + + clinic = new Clinic("Djurkliniken", "Storgatan 1", "+4670123456"); + setPrivateField(clinic, "id", clinicId); + + ownerUser = new User("Anna Ägare", "anna@example.se", "hashedPw", Role.OWNER); + setPrivateField(ownerUser, "id", userId); + + vetUser = new User("Dr. Vet", "vet@klinik.se", "hashedPw", Role.VET, clinic); + setPrivateField(vetUser, "id", UUID.randomUUID()); + + adminUser = new User("Admin Adminsson", "admin@example.se", "hashedPw", Role.ADMIN); + setPrivateField(adminUser, "id", UUID.randomUUID()); + } + + // createUser + // ───────────────────────────────────────────────────────────────────────── + + @Test + void createUser_owner_withoutClinic_returnsResponse() { + UserRequest request = ownerRequest("anna@example.se", null); + + when(userRepository.existsByEmail("anna@example.se")).thenReturn(false); + when(userRepository.save(any(User.class))).thenReturn(ownerUser); + + UserResponse response = userService.createUser(request); + + assertThat(response.getEmail()).isEqualTo("anna@example.se"); + assertThat(response.getRole()).isEqualTo(Role.OWNER); + assertThat(response.getClinicId()).isNull(); + verify(userRepository).save(any(User.class)); + } + + @Test + void createUser_vet_withClinic_returnsResponse() { + UserRequest request = vetRequest("vet@klinik.se", clinicId); + + when(userRepository.existsByEmail("vet@klinik.se")).thenReturn(false); + when(clinicRepository.findById(clinicId)).thenReturn(Optional.of(clinic)); + when(userRepository.save(any(User.class))).thenReturn(vetUser); + + UserResponse response = userService.createUser(request); + + assertThat(response.getRole()).isEqualTo(Role.VET); + assertThat(response.getClinicId()).isEqualTo(clinicId); + } + + @Test + void createUser_emailAlreadyExists_throwsBusinessRuleException() { + UserRequest request = ownerRequest("anna@example.se", null); + when(userRepository.existsByEmail("anna@example.se")).thenReturn(true); + + assertThatThrownBy(() -> userService.createUser(request)) + .isInstanceOf(BusinessRuleException.class) + .hasMessageContaining("Email används redan"); + + verify(userRepository, never()).save(any()); + } + + @Test + void createUser_vet_withoutClinic_throwsBusinessRuleException() { + UserRequest request = vetRequest("vet@klinik.se", null); + when(userRepository.existsByEmail("vet@klinik.se")).thenReturn(false); + + assertThatThrownBy(() -> userService.createUser(request)) + .isInstanceOf(BusinessRuleException.class) + .hasMessageContaining("Veterinär måste vara kopplad till en klinik"); + } + + @Test + void createUser_owner_withClinic_throwsBusinessRuleException() { + UserRequest request = ownerRequest("anna@example.se", clinicId); + when(userRepository.existsByEmail("anna@example.se")).thenReturn(false); + + assertThatThrownBy(() -> userService.createUser(request)) + .isInstanceOf(BusinessRuleException.class) + .hasMessageContaining("Endast veterinärer kan kopplas till en klinik"); + } + + @Test + void createUser_vet_clinicNotFound_throwsResourceNotFoundException() { + UUID unknownClinicId = UUID.randomUUID(); + UserRequest request = vetRequest("vet@klinik.se", unknownClinicId); + + when(userRepository.existsByEmail("vet@klinik.se")).thenReturn(false); + when(clinicRepository.findById(unknownClinicId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> userService.createUser(request)) + .isInstanceOf(ResourceNotFoundException.class); + } + + @Test + void createUser_dataIntegrityViolation_throwsBusinessRuleException() { + UserRequest request = ownerRequest("anna@example.se", null); + + when(userRepository.existsByEmail("anna@example.se")).thenReturn(false); + when(userRepository.save(any(User.class))).thenThrow(new DataIntegrityViolationException("dup")); + + assertThatThrownBy(() -> userService.createUser(request)) + .isInstanceOf(BusinessRuleException.class) + .hasMessageContaining("Email används redan"); + } + + + //Helper + private void setPrivateField(Object target, String fieldName, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } + + private UserRequest vetRequest(String email, UUID clinicId) { + UserRequest req = new UserRequest(); + req.setName("Dr. Vet"); + req.setEmail(email); + req.setPassword("veterinär123"); + req.setRole(Role.VET); + req.setClinicId(clinicId); + return req; + } + + private UserRequest ownerRequest(String email, UUID clinicId) { + UserRequest req = new UserRequest(); + req.setName("Anna Ägare"); + req.setEmail(email); + req.setPassword("lösenord123"); + req.setRole(Role.OWNER); + req.setClinicId(clinicId); + return req; + } + +} From 2084b61966f67ad9cd31abb0b911cf2293e5a575 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Thu, 9 Apr 2026 12:34:01 +0200 Subject: [PATCH 10/85] test: test updateUser --- .../vet1177/services/UserServiceTest.java | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/src/test/java/org/example/vet1177/services/UserServiceTest.java b/src/test/java/org/example/vet1177/services/UserServiceTest.java index bdab2658..25381df2 100644 --- a/src/test/java/org/example/vet1177/services/UserServiceTest.java +++ b/src/test/java/org/example/vet1177/services/UserServiceTest.java @@ -1,6 +1,7 @@ package org.example.vet1177.services; import org.example.vet1177.dto.request.user.UserRequest; +import org.example.vet1177.dto.request.user.UserUpdateRequest; import org.example.vet1177.dto.response.user.UserResponse; import org.example.vet1177.entities.Clinic; import org.example.vet1177.entities.Role; @@ -157,6 +158,103 @@ void createUser_dataIntegrityViolation_throwsBusinessRuleException() { .hasMessageContaining("Email används redan"); } + //updateUser + + @Test + void updateUser_changeName_updatesAndReturns() { + UserUpdateRequest request = new UserUpdateRequest(); + request.setName("Nytt Namn"); + + when(userRepository.findById(userId)).thenReturn(Optional.of(ownerUser)); + when(userRepository.save(ownerUser)).thenReturn(ownerUser); + + UserResponse response = userService.updateUser(userId, request); + + assertThat(ownerUser.getName()).isEqualTo("Nytt Namn"); + verify(userRepository).save(ownerUser); + } + + @Test + void updateUser_changeEmail_uniqueEmail_updatesAndReturns() { + UserUpdateRequest request = new UserUpdateRequest(); + request.setEmail("ny@example.se"); + + when(userRepository.findById(userId)).thenReturn(Optional.of(ownerUser)); + when(userRepository.existsByEmailAndIdNot("ny@example.se", userId)).thenReturn(false); + when(userRepository.save(ownerUser)).thenReturn(ownerUser); + + userService.updateUser(userId, request); + + assertThat(ownerUser.getEmail()).isEqualTo("ny@example.se"); + } + + @Test + void updateUser_changeEmail_duplicateEmail_throwsBusinessRuleException() { + UserUpdateRequest request = new UserUpdateRequest(); + request.setEmail("tagen@example.se"); + + when(userRepository.findById(userId)).thenReturn(Optional.of(ownerUser)); + when(userRepository.existsByEmailAndIdNot("tagen@example.se", userId)).thenReturn(true); + + assertThatThrownBy(() -> userService.updateUser(userId, request)) + .isInstanceOf(BusinessRuleException.class) + .hasMessageContaining("Email används redan"); + + verify(userRepository, never()).save(any()); + } + + @Test + void updateUser_vet_changeClinic_updatesClinic() { + UUID newClinicId = UUID.randomUUID(); + Clinic newClinic = new Clinic("Ny Klinik", "Nya gatan 2", "+4670000000"); + + UserUpdateRequest request = new UserUpdateRequest(); + request.setClinicId(newClinicId); + + when(userRepository.findById(vetUser.getId())).thenReturn(Optional.of(vetUser)); + when(clinicRepository.findById(newClinicId)).thenReturn(Optional.of(newClinic)); + when(userRepository.save(vetUser)).thenReturn(vetUser); + + userService.updateUser(vetUser.getId(), request); + + assertThat(vetUser.getClinic()).isEqualTo(newClinic); + } + + @Test + void updateUser_owner_setClinic_throwsBusinessRuleException() { + UserUpdateRequest request = new UserUpdateRequest(); + request.setClinicId(clinicId); + + when(userRepository.findById(userId)).thenReturn(Optional.of(ownerUser)); + + assertThatThrownBy(() -> userService.updateUser(userId, request)) + .isInstanceOf(BusinessRuleException.class) + .hasMessageContaining("Endast veterinärer kan kopplas till en klinik"); + } + + @Test + void updateUser_unknownId_throwsResourceNotFoundException() { + UUID unknownId = UUID.randomUUID(); + when(userRepository.findById(unknownId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> userService.updateUser(unknownId, new UserUpdateRequest())) + .isInstanceOf(ResourceNotFoundException.class); + } + + @Test + void updateUser_dataIntegrityViolation_throwsBusinessRuleException() { + UserUpdateRequest request = new UserUpdateRequest(); + request.setEmail("ny@example.se"); + + when(userRepository.findById(userId)).thenReturn(Optional.of(ownerUser)); + when(userRepository.existsByEmailAndIdNot("ny@example.se", userId)).thenReturn(false); + when(userRepository.save(any())).thenThrow(new DataIntegrityViolationException("dup")); + + assertThatThrownBy(() -> userService.updateUser(userId, request)) + .isInstanceOf(BusinessRuleException.class) + .hasMessageContaining("Email används redan"); + } + //Helper private void setPrivateField(Object target, String fieldName, Object value) throws Exception { From dceac9a49cef763d7ec9eac60b1c007a9d8f36d4 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Thu, 9 Apr 2026 13:11:33 +0200 Subject: [PATCH 11/85] test: test deleteUser --- .../vet1177/services/UserServiceTest.java | 117 +++++++++++++++++- 1 file changed, 115 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/example/vet1177/services/UserServiceTest.java b/src/test/java/org/example/vet1177/services/UserServiceTest.java index 25381df2..c922d6c4 100644 --- a/src/test/java/org/example/vet1177/services/UserServiceTest.java +++ b/src/test/java/org/example/vet1177/services/UserServiceTest.java @@ -9,6 +9,8 @@ import org.example.vet1177.exception.BusinessRuleException; import org.example.vet1177.exception.ResourceNotFoundException; import org.example.vet1177.repository.ClinicRepository; +import org.example.vet1177.repository.MedicalRecordRepository; +import org.example.vet1177.repository.PetRepository; import org.example.vet1177.repository.UserRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -37,10 +39,10 @@ public class UserServiceTest { private ClinicRepository clinicRepository; @Mock - private PetService petService; + private PetRepository petRepository; @Mock - private MedicalRecordService medicalRecordService; + private MedicalRecordRepository medicalRecordRepository; @InjectMocks private UserService userService; @@ -255,6 +257,117 @@ void updateUser_dataIntegrityViolation_throwsBusinessRuleException() { .hasMessageContaining("Email används redan"); } + //DeleteUser + + @Test + void deleteUser_noConstraints_deletesSuccessfully() { + when(userRepository.findById(userId)).thenReturn(Optional.of(ownerUser)); + when(petRepository.existsByOwner_Id(userId)).thenReturn(false); + when(medicalRecordRepository.existsByOwnerId(userId)).thenReturn(false); + when(medicalRecordRepository.existsByAssignedVetId(userId)).thenReturn(false); + when(medicalRecordRepository.existsByCreatedById(userId)).thenReturn(false); + when(medicalRecordRepository.existsByUpdatedById(userId)).thenReturn(false); + + userService.deleteUser(userId); + + verify(userRepository).delete(ownerUser); + } + + @Test + void deleteUser_unknownId_throwsResourceNotFoundException() { + UUID unknownId = UUID.randomUUID(); + when(userRepository.findById(unknownId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> userService.deleteUser(unknownId)) + .isInstanceOf(ResourceNotFoundException.class); + + verify(userRepository, never()).delete(any()); + } + + @Test + void deleteUser_userHasPets_throwsBusinessRuleException() { + when(userRepository.findById(userId)).thenReturn(Optional.of(ownerUser)); + when(petRepository.existsByOwner_Id(userId)).thenReturn(true); + + assertThatThrownBy(() -> userService.deleteUser(userId)) + .isInstanceOf(BusinessRuleException.class) + .hasMessageContaining("kopplade djur"); + + verify(userRepository, never()).delete(any()); + } + + @Test + void deleteUser_userIsRecordOwner_throwsBusinessRuleException() { + when(userRepository.findById(userId)).thenReturn(Optional.of(ownerUser)); + when(petRepository.existsByOwner_Id(userId)).thenReturn(false); + when(medicalRecordRepository.existsByOwnerId(userId)).thenReturn(true); + + assertThatThrownBy(() -> userService.deleteUser(userId)) + .isInstanceOf(BusinessRuleException.class) + .hasMessageContaining("ägare på journalposter"); + + verify(userRepository, never()).delete(any()); + } + + @Test + void deleteUser_userIsAssignedVet_throwsBusinessRuleException() { + when(userRepository.findById(userId)).thenReturn(Optional.of(ownerUser)); + when(petRepository.existsByOwner_Id(userId)).thenReturn(false); + when(medicalRecordRepository.existsByOwnerId(userId)).thenReturn(false); + when(medicalRecordRepository.existsByAssignedVetId(userId)).thenReturn(true); + + assertThatThrownBy(() -> userService.deleteUser(userId)) + .isInstanceOf(BusinessRuleException.class) + .hasMessageContaining("tilldelad veterinär"); + + verify(userRepository, never()).delete(any()); + } + + @Test + void deleteUser_userCreatedRecords_throwsBusinessRuleException() { + when(userRepository.findById(userId)).thenReturn(Optional.of(ownerUser)); + when(petRepository.existsByOwner_Id(userId)).thenReturn(false); + when(medicalRecordRepository.existsByOwnerId(userId)).thenReturn(false); + when(medicalRecordRepository.existsByAssignedVetId(userId)).thenReturn(false); + when(medicalRecordRepository.existsByCreatedById(userId)).thenReturn(true); + + assertThatThrownBy(() -> userService.deleteUser(userId)) + .isInstanceOf(BusinessRuleException.class) + .hasMessageContaining("skapat journalposter"); + + verify(userRepository, never()).delete(any()); + } + + @Test + void deleteUser_userUpdatedRecords_throwsBusinessRuleException() { + when(userRepository.findById(userId)).thenReturn(Optional.of(ownerUser)); + when(petRepository.existsByOwner_Id(userId)).thenReturn(false); + when(medicalRecordRepository.existsByOwnerId(userId)).thenReturn(false); + when(medicalRecordRepository.existsByAssignedVetId(userId)).thenReturn(false); + when(medicalRecordRepository.existsByCreatedById(userId)).thenReturn(false); + when(medicalRecordRepository.existsByUpdatedById(userId)).thenReturn(true); + + assertThatThrownBy(() -> userService.deleteUser(userId)) + .isInstanceOf(BusinessRuleException.class) + .hasMessageContaining("uppdaterat journalposter"); + + verify(userRepository, never()).delete(any()); + } + + @Test + void deleteUser_dataIntegrityViolation_throwsBusinessRuleException() { + when(userRepository.findById(userId)).thenReturn(Optional.of(ownerUser)); + when(petRepository.existsByOwner_Id(userId)).thenReturn(false); + when(medicalRecordRepository.existsByOwnerId(userId)).thenReturn(false); + when(medicalRecordRepository.existsByAssignedVetId(userId)).thenReturn(false); + when(medicalRecordRepository.existsByCreatedById(userId)).thenReturn(false); + when(medicalRecordRepository.existsByUpdatedById(userId)).thenReturn(false); + doThrow(new DataIntegrityViolationException("fk")).when(userRepository).delete(ownerUser); + + assertThatThrownBy(() -> userService.deleteUser(userId)) + .isInstanceOf(BusinessRuleException.class) + .hasMessageContaining("kopplade poster"); + } //Helper private void setPrivateField(Object target, String fieldName, Object value) throws Exception { From ccfdf95608975cb4e0f813e77e9cc2a8516ce3ec Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Thu, 9 Apr 2026 13:58:35 +0200 Subject: [PATCH 12/85] test: add getbyemail, getuserEntitybyId, GetbyId, GetAllUsers. fix from rabbit --- .../vet1177/services/UserServiceTest.java | 95 ++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/example/vet1177/services/UserServiceTest.java b/src/test/java/org/example/vet1177/services/UserServiceTest.java index c922d6c4..46a64088 100644 --- a/src/test/java/org/example/vet1177/services/UserServiceTest.java +++ b/src/test/java/org/example/vet1177/services/UserServiceTest.java @@ -15,12 +15,14 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.dao.DataIntegrityViolationException; import java.lang.reflect.Field; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -87,7 +89,11 @@ void createUser_owner_withoutClinic_returnsResponse() { assertThat(response.getEmail()).isEqualTo("anna@example.se"); assertThat(response.getRole()).isEqualTo(Role.OWNER); assertThat(response.getClinicId()).isNull(); - verify(userRepository).save(any(User.class)); + ArgumentCaptor captor = ArgumentCaptor.forClass(User.class); + verify(userRepository).save(captor.capture()); + assertThat(captor.getValue().getPassword()) + .as("lösenordet ska vara hashat innan persist") + .isNotEqualTo(request.getPassword()); } @Test @@ -160,6 +166,93 @@ void createUser_dataIntegrityViolation_throwsBusinessRuleException() { .hasMessageContaining("Email används redan"); } + //getByEmail + + + @Test + void getByEmail_existingEmail_returnsUser() { + when(userRepository.findByEmail("anna@example.se")).thenReturn(Optional.of(ownerUser)); + + User result = userService.getByEmail("anna@example.se"); + + assertThat(result).isEqualTo(ownerUser); + } + + @Test + void getByEmail_unknownEmail_throwsResourceNotFoundException() { + when(userRepository.findByEmail("okänd@example.se")).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> userService.getByEmail("okänd@example.se")) + .isInstanceOf(ResourceNotFoundException.class); + } + + + // getUserEntityById + + @Test + void getUserEntityById_existingId_returnsUserEntity() { + when(userRepository.findById(userId)).thenReturn(Optional.of(ownerUser)); + + User result = userService.getUserEntityById(userId); + + assertThat(result).isEqualTo(ownerUser); + } + + @Test + void getUserEntityById_unknownId_throwsResourceNotFoundException() { + UUID unknownId = UUID.randomUUID(); + when(userRepository.findById(unknownId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> userService.getUserEntityById(unknownId)) + .isInstanceOf(ResourceNotFoundException.class); + } + + + // getById + + @Test + void getById_existingId_returnsUserResponse() { + when(userRepository.findById(userId)).thenReturn(Optional.of(ownerUser)); + + UserResponse response = userService.getById(userId); + + assertThat(response.getId()).isEqualTo(ownerUser.getId()); + assertThat(response.getEmail()).isEqualTo("anna@example.se"); + assertThat(response.getRole()).isEqualTo(Role.OWNER); + } + + @Test + void getById_unknownId_throwsResourceNotFoundException() { + UUID unknownId = UUID.randomUUID(); + when(userRepository.findById(unknownId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> userService.getById(unknownId)) + .isInstanceOf(ResourceNotFoundException.class); + } + + // getAllUsers + + @Test + void getAllUsers_returnsAllMappedResponses() { + when(userRepository.findAll()).thenReturn(List.of(ownerUser, vetUser, adminUser)); + + List responses = userService.getAllUsers(); + + assertThat(responses).hasSize(3); + assertThat(responses).extracting(UserResponse::getRole) + .containsExactlyInAnyOrder(Role.OWNER, Role.VET, Role.ADMIN); + } + + @Test + void getAllUsers_emptyRepo_returnsEmptyList() { + when(userRepository.findAll()).thenReturn(List.of()); + + List responses = userService.getAllUsers(); + + assertThat(responses).isEmpty(); + } + + //updateUser @Test From 0fea6e0150e57c0a1602c8dd6a5476673f0424f3 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Thu, 9 Apr 2026 14:46:31 +0200 Subject: [PATCH 13/85] test: add PetController tests for createPet endpoint --- .../vet1177/controller/PetControllerTest.java | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 src/test/java/org/example/vet1177/controller/PetControllerTest.java diff --git a/src/test/java/org/example/vet1177/controller/PetControllerTest.java b/src/test/java/org/example/vet1177/controller/PetControllerTest.java new file mode 100644 index 00000000..6d273716 --- /dev/null +++ b/src/test/java/org/example/vet1177/controller/PetControllerTest.java @@ -0,0 +1,115 @@ +package org.example.vet1177.controller; +import tools.jackson.databind.ObjectMapper; +import org.example.vet1177.dto.request.pet.PetRequest; +import org.example.vet1177.entities.Pet; +import org.example.vet1177.entities.Role; +import org.example.vet1177.entities.User; +import org.example.vet1177.exception.ForbiddenException; +import org.example.vet1177.security.SecurityConfig; +import org.example.vet1177.services.PetService; +import org.example.vet1177.services.UserService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.RequestPostProcessor; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(PetController.class) +@Import(SecurityConfig.class) +public class PetControllerTest { + + @Autowired + private MockMvc mockMvc; + @Autowired + private ObjectMapper objectMapper; + @MockitoBean + private PetService petService; + @MockitoBean + private UserService userService; + + private User owner; + private User vet; + private Pet pet; + private UUID ownerId; + private UUID petId; + + @BeforeEach + void setUp() { + ownerId = UUID.randomUUID(); + petId = UUID.randomUUID(); + owner = new User("Anna Ägare", "anna@mail.se", "hash", Role.OWNER); + vet = new User("Dr. Erik Vet", "erik@vet.se", "hash", Role.VET); + pet = new Pet(owner, "Molly", "Hund", "Labrador", LocalDate.of(2020, 1, 1), new BigDecimal("12.50")); + } + private RequestPostProcessor authenticatedAs(User user) { + return authentication(new UsernamePasswordAuthenticationToken( + user, null, user.getAuthorities() + )); + } + + private PetRequest validPetRequest() { + PetRequest request = new PetRequest(); + request.setName("Molly"); + request.setSpecies("Hund"); + request.setBreed("Labrador"); + request.setDateOfBirth(LocalDate.of(2020, 1, 1)); + request.setWeightKg(new BigDecimal("12.50")); + return request; + } + + //POST /pets + @Test + void createPet_shouldReturn200WithPetResponse() throws Exception { + when(petService.createPet(any(), any(), any())).thenReturn(pet); + + mockMvc.perform(post("/pets") + .with(authenticatedAs(owner)) + .header("currentUserId", ownerId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(validPetRequest()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("Molly")); + } + + @Test + void createPet_whenInvalidRequest_shouldReturn400() throws Exception { + PetRequest request = validPetRequest(); + request.setName(""); + + mockMvc.perform(post("/pets") + .with(authenticatedAs(owner)) + .header("currentUserId", ownerId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + void createPet_whenForbidden_shouldReturn403() throws Exception { + when(petService.createPet(any(), any(), any())) + .thenThrow(new ForbiddenException("Du kan inte lägga till ett djur")); + + mockMvc.perform(post("/pets") + .with(authenticatedAs(vet)) + .header("currentUserId", UUID.randomUUID()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(validPetRequest()))) + .andExpect(status().isForbidden()); + } + +} From 0d98cf1972678082706bee215ad35ba1e4cca687 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Thu, 9 Apr 2026 15:28:05 +0200 Subject: [PATCH 14/85] test: add tests for GET /pets/{petId} --- .../vet1177/controller/PetControllerTest.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/test/java/org/example/vet1177/controller/PetControllerTest.java b/src/test/java/org/example/vet1177/controller/PetControllerTest.java index 6d273716..ee922a7e 100644 --- a/src/test/java/org/example/vet1177/controller/PetControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/PetControllerTest.java @@ -1,4 +1,5 @@ package org.example.vet1177.controller; +import org.example.vet1177.exception.ResourceNotFoundException; import tools.jackson.databind.ObjectMapper; import org.example.vet1177.dto.request.pet.PetRequest; import org.example.vet1177.entities.Pet; @@ -111,5 +112,42 @@ void createPet_whenForbidden_shouldReturn403() throws Exception { .content(objectMapper.writeValueAsString(validPetRequest()))) .andExpect(status().isForbidden()); } + // GET /pets/{petId} + + @Test + void getPetById_shouldReturn200WithPetResponse() throws Exception { + when(userService.getUserEntityById(any())).thenReturn(owner); + when(petService.getPetById(any(), any())).thenReturn(pet); + + mockMvc.perform(get("/pets/{petId}", petId) + .with(authenticatedAs(owner)) + .header("currentUserId", ownerId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("Molly")); + } + + @Test + void getPetById_whenNotFound_shouldReturn404() throws Exception { + when(userService.getUserEntityById(any())).thenReturn(owner); + when(petService.getPetById(any(), any())) + .thenThrow(new ResourceNotFoundException("Pet", petId)); + + mockMvc.perform(get("/pets/{petId}", petId) + .with(authenticatedAs(owner)) + .header("currentUserId", ownerId)) + .andExpect(status().isNotFound()); + } + + @Test + void getPetById_whenForbidden_shouldReturn403() throws Exception { + when(userService.getUserEntityById(any())).thenReturn(vet); + when(petService.getPetById(any(), any())) + .thenThrow(new ForbiddenException("Du har inte behörighet att se detta djur")); + + mockMvc.perform(get("/pets/{petId}", petId) + .with(authenticatedAs(vet)) + .header("currentUserId", UUID.randomUUID())) + .andExpect(status().isForbidden()); + } } From 71da4c0b4f3183feb8577c4af2367c08415a3a11 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Thu, 9 Apr 2026 16:32:33 +0200 Subject: [PATCH 15/85] test: add tests for PUT /pets/{petId} and GET /pets/owner/{ownerId} --- .../vet1177/controller/PetControllerTest.java | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/test/java/org/example/vet1177/controller/PetControllerTest.java b/src/test/java/org/example/vet1177/controller/PetControllerTest.java index ee922a7e..8f1488f7 100644 --- a/src/test/java/org/example/vet1177/controller/PetControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/PetControllerTest.java @@ -22,6 +22,7 @@ import java.math.BigDecimal; import java.time.LocalDate; +import java.util.List; import java.util.UUID; import static org.mockito.ArgumentMatchers.any; @@ -149,5 +150,71 @@ void getPetById_whenForbidden_shouldReturn403() throws Exception { .header("currentUserId", UUID.randomUUID())) .andExpect(status().isForbidden()); } + // GET /pets/owner/{ownerId} + + @Test + void getPetsByOwner_shouldReturn200WithList() throws Exception { + when(petService.getPetsByOwner(any(), any())).thenReturn(List.of(pet)); + + mockMvc.perform(get("/pets/owner/{ownerId}", ownerId) + .with(authenticatedAs(owner)) + .header("currentUserId", ownerId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].name").value("Molly")); + } + + @Test + void getPetsByOwner_whenForbidden_shouldReturn403() throws Exception { + when(petService.getPetsByOwner(any(), any())) + .thenThrow(new ForbiddenException("Du saknar behörighet")); + + mockMvc.perform(get("/pets/owner/{ownerId}", ownerId) + .with(authenticatedAs(vet)) + .header("currentUserId", UUID.randomUUID())) + .andExpect(status().isForbidden()); + } + + // PUT /pets/{petId} + + @Test + void updatePet_shouldReturn200WithUpdatedPetResponse() throws Exception { + Pet updated = new Pet(owner, "Harry", "Katt", "Siamese", LocalDate.of(2021, 3, 5), new BigDecimal("5.00")); + when(petService.updatePet(any(), any(), any())).thenReturn(updated); + + mockMvc.perform(put("/pets/{petId}", petId) + .with(authenticatedAs(owner)) + .header("currentUserId", ownerId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(validPetRequest()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("Harry")); + } + + @Test + void updatePet_whenNotFound_shouldReturn404() throws Exception { + when(petService.updatePet(any(), any(), any())) + .thenThrow(new ResourceNotFoundException("Pet", petId)); + + mockMvc.perform(put("/pets/{petId}", petId) + .with(authenticatedAs(owner)) + .header("currentUserId", ownerId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(validPetRequest()))) + .andExpect(status().isNotFound()); + } + + @Test + void updatePet_whenForbidden_shouldReturn403() throws Exception { + when(petService.updatePet(any(), any(), any())) + .thenThrow(new ForbiddenException("Du saknar behörighet")); + + mockMvc.perform(put("/pets/{petId}", petId) + .with(authenticatedAs(vet)) + .header("currentUserId", UUID.randomUUID()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(validPetRequest()))) + .andExpect(status().isForbidden()); + } + } From 8db8572c362678532b9a93ae88637b26e8328465 Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Fri, 10 Apr 2026 10:21:20 +0200 Subject: [PATCH 16/85] feat: add OAuth2 resource server dependency in pom.xml --- pom.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pom.xml b/pom.xml index 374cc6cf..a44981a0 100644 --- a/pom.xml +++ b/pom.xml @@ -81,6 +81,10 @@ org.springframework.boot spring-boot-starter-security + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + software.amazon.awssdk s3 From 90517ef9e05552550fb558ae696d92d9997dab28 Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Fri, 10 Apr 2026 10:22:38 +0200 Subject: [PATCH 17/85] feat: add JWT configuration properties in application.properties --- src/main/resources/application.properties | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 87967623..b926dda2 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -27,4 +27,8 @@ aws.s3.bucket-name=${S3_BUCKET:} aws.s3.region=${S3_REGION:} spring.servlet.multipart.max-file-size=10MB -spring.servlet.multipart.max-request-size=10MB \ No newline at end of file +spring.servlet.multipart.max-request-size=10MB + +# JWT Configuration +jwt.secret-key=${JWT_SECRET:default-dev-secret-key-change-in-production-min-32-chars!!} +jwt.expiration-ms=86400000 \ No newline at end of file From c8c05ee4e8c0437b9a2029f117d9f9839b8c935b Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Fri, 10 Apr 2026 16:32:19 +0200 Subject: [PATCH 18/85] feat: add JwtProperties to load JWT configuration from application.properties --- .../vet1177/security/JwtProperties.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/main/java/org/example/vet1177/security/JwtProperties.java diff --git a/src/main/java/org/example/vet1177/security/JwtProperties.java b/src/main/java/org/example/vet1177/security/JwtProperties.java new file mode 100644 index 00000000..1c165f55 --- /dev/null +++ b/src/main/java/org/example/vet1177/security/JwtProperties.java @@ -0,0 +1,22 @@ +package org.example.vet1177.security; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Läser JWT-konfiguration från application.properties. + * + * jwt.secret-key → den hemliga nyckeln som används för att signera/verifiera tokens (HMAC SHA-256) + * jwt.expiration-ms → hur länge en token är giltig i millisekunder (86400000 = 24 timmar) + */ +@ConfigurationProperties(prefix = "jwt") +public class JwtProperties { + + private String secretKey; + private long expirationMs; + + public String getSecretKey() { return secretKey; } + public void setSecretKey(String secretKey) { this.secretKey = secretKey; } + + public long getExpirationMs() { return expirationMs; } + public void setExpirationMs(long expirationMs) { this.expirationMs = expirationMs; } +} From 56c6df570937c1577d273d0cdea9c8ea04904f72 Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Fri, 10 Apr 2026 16:33:29 +0200 Subject: [PATCH 19/85] feat: add JwtService for handling JWT creation and validation - Implements generating JWT tokens with claims for user authentication. - Adds decoding and validation of tokens with HS256 algorithm. - Exposes JwtDecoder as a bean for Spring Security integration. --- .../example/vet1177/security/JwtService.java | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/main/java/org/example/vet1177/security/JwtService.java diff --git a/src/main/java/org/example/vet1177/security/JwtService.java b/src/main/java/org/example/vet1177/security/JwtService.java new file mode 100644 index 00000000..cbb3233a --- /dev/null +++ b/src/main/java/org/example/vet1177/security/JwtService.java @@ -0,0 +1,114 @@ +package org.example.vet1177.security; + +import com.nimbusds.jose.jwk.source.ImmutableSecret; +import org.example.vet1177.entities.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.oauth2.jose.jws.MacAlgorithm; +import org.springframework.security.oauth2.jwt.*; +import org.springframework.stereotype.Service; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.time.Instant; + +/** + * Hanterar skapande och validering av JWT-tokens. + * + * Flödet: + * 1. Användaren loggar in med email + lösenord + * 2. AuthService anropar generateToken(user) → får tillbaka en JWT-sträng + * 3. Frontend skickar JWT:n i varje request: "Authorization: Bearer " + * 4. JwtAuthenticationFilter fångar headern, anropar decode(token) → får tillbaka claims + * 5. Filtret laddar användaren från DB och sätter den i SecurityContext + * + * Vi använder HS256 (HMAC med SHA-256) — en symmetrisk algoritm där samma + * hemliga nyckel används för att både signera och verifiera tokens. + */ +@Service +public class JwtService { + + private static final Logger log = LoggerFactory.getLogger(JwtService.class); + + private final JwtEncoder jwtEncoder; + private final JwtDecoder jwtDecoder; + private final long expirationMs; + + public JwtService(JwtProperties jwtProperties) { + // Skapar en kryptografisk nyckel från vår hemliga sträng i application.properties. + // SecretKeySpec tar emot byte-arrayen + algoritmen och ger oss ett SecretKey-objekt + // som Java:s krypto-API kan använda. + SecretKey key = new SecretKeySpec( + jwtProperties.getSecretKey().getBytes(), + "HmacSHA256" + ); + + // JwtEncoder — skapar nya tokens. + // ImmutableSecret wrapprar vår nyckel så att Nimbus-biblioteket kan använda den. + this.jwtEncoder = new NimbusJwtEncoder(new ImmutableSecret<>(key)); + + // JwtDecoder — avkodar och validerar befintliga tokens. + // Den kontrollerar automatiskt: signatur (är tokenen äkta?) och utgångstid (har den gått ut?). + this.jwtDecoder = NimbusJwtDecoder.withSecretKey(key) + .macAlgorithm(MacAlgorithm.HS256) + .build(); + + this.expirationMs = jwtProperties.getExpirationMs(); + } + + /** + * Skapar en ny JWT för en inloggad användare. + * + * Tokenen innehåller "claims" — nyckel-värde-par med information: + * - sub (subject): användarens email — det unika identifieringsvärdet + * - userId: UUID:t — behövs för att hämta användaren från DB + * - role: ROLE_VET / ROLE_OWNER / ROLE_ADMIN — för rollbaserad åtkomstkontroll + * - iat (issued at): när tokenen skapades + * - exp (expires at): när tokenen slutar gälla + */ + public String generateToken(User user) { + Instant now = Instant.now(); + + JwtClaimsSet claims = JwtClaimsSet.builder() + .subject(user.getEmail()) + .claim("userId", user.getId().toString()) + .claim("role", "ROLE_" + user.getRole().name()) + .issuedAt(now) + .expiresAt(now.plusMillis(expirationMs)) + .build(); + + // Talar om att vi vill signera med HS256-algoritmen + JwtEncoderParameters params = JwtEncoderParameters.from( + JwsHeader.with(MacAlgorithm.HS256).build(), + claims + ); + + String token = jwtEncoder.encode(params).getTokenValue(); + log.info("Generated JWT for user email={}", user.getEmail()); + return token; + } + + /** + * Avkodar och validerar en JWT-sträng. + * + * Nimbus-biblioteket gör automatiskt: + * 1. Kontrollerar att signaturen stämmer (ingen har ändrat innehållet) + * 2. Kontrollerar att tokenen inte har gått ut (exp > nu) + * + * Om något är fel kastas JwtException och anroparen vet att tokenen är ogiltig. + * + * @return Jwt-objekt med alla claims om tokenen är giltig + * @throws JwtException om tokenen är ogiltig, manipulerad eller utgången + */ + public Jwt decode(String token) { + return jwtDecoder.decode(token); + } + + /** + * Exponerar JwtDecoder som en bean som Spring Security kan använda. + * SecurityConfig behöver en JwtDecoder-bean för att konfigurera oauth2ResourceServer(). + */ + public JwtDecoder getJwtDecoder() { + return jwtDecoder; + } +} From 3a6a56b329e04c432b6f797375913f7328834878 Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Sat, 11 Apr 2026 18:22:57 +0200 Subject: [PATCH 20/85] feat: add JwtAuthenticationFilter for token-based request authentication - Implements a filter to authenticate HTTP requests using JWT tokens. - Extracts and validates tokens from the Authorization header. - Loads user details and sets authentication in SecurityContext. --- .../security/JwtAuthenticationFilter.java | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/main/java/org/example/vet1177/security/JwtAuthenticationFilter.java diff --git a/src/main/java/org/example/vet1177/security/JwtAuthenticationFilter.java b/src/main/java/org/example/vet1177/security/JwtAuthenticationFilter.java new file mode 100644 index 00000000..c0d3b3df --- /dev/null +++ b/src/main/java/org/example/vet1177/security/JwtAuthenticationFilter.java @@ -0,0 +1,123 @@ +package org.example.vet1177.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtException; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +/** + * Filter som körs EN gång per HTTP-request (OncePerRequestFilter). + * + * Vad händer steg för steg: + * + * [Klient] → HTTP Request med "Authorization: Bearer eyJhbG..." + * ↓ + * [JwtAuthenticationFilter] + * 1. Läser Authorization-headern + * 2. Extraherar token-strängen (allt efter "Bearer ") + * 3. Avkodar token via JwtService → får email + roll + * 4. Laddar User från DB via CustomUserDetailsService + * 5. Skapar ett Authentication-objekt och sätter det i SecurityContext + * ↓ + * [Spring Security] + * Kollar SecurityContext: finns en autentiserad användare? + * Om ja → kontrollerar att användaren har rätt roll för endpointen + * Om nej → returnerar 401 Unauthorized + * ↓ + * [Controller] + * @AuthenticationPrincipal User currentUser ← hämtas från SecurityContext + */ +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private static final Logger log = LoggerFactory.getLogger(JwtAuthenticationFilter.class); + + private final JwtService jwtService; + private final CustomUserDetailsService userDetailsService; + + public JwtAuthenticationFilter(JwtService jwtService, CustomUserDetailsService userDetailsService) { + this.jwtService = jwtService; + this.userDetailsService = userDetailsService; + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + // 1. Läs Authorization-headern + // Formatet är: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + String authHeader = request.getHeader("Authorization"); + + // Om headern saknas eller inte börjar med "Bearer " → skippa filtret. + // Requesten släpps vidare i kedjan. Om endpointen kräver auth + // kommer Spring Security att returnera 401 automatiskt. + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + // 2. Extrahera token-strängen (ta bort "Bearer " som är 7 tecken) + String token = authHeader.substring(7); + + try { + // 3. Avkoda och validera token. + // Om signaturen inte stämmer eller token har gått ut kastar decode() JwtException. + Jwt jwt = jwtService.decode(token); + + // 4. Hämta email från token (subject-claim) + String email = jwt.getSubject(); + + // Kolla att vi inte redan har autentiserat denna request + // (kan hända om flera filter kör i kedjan) + if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) { + + // 5. Ladda det fulla User-objektet från databasen. + // Vi behöver hela entiteten (med id, klinik, roll) för @AuthenticationPrincipal. + var userDetails = userDetailsService.loadUserByUsername(email); + + // 6. Skapa ett Authentication-objekt. + // UsernamePasswordAuthenticationToken är Springs standardklass för "en autentiserad användare". + // - Första argumentet (principal) → User-objektet (det som @AuthenticationPrincipal ger) + // - Andra argumentet (credentials) → null (vi behöver inte lösenordet, token räcker) + // - Tredje argumentet (authorities) → rollerna (ROLE_VET, ROLE_ADMIN, etc.) + var authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities() + ); + + // Kopplar request-detaljer (IP-adress, session-id) till autentiseringen — för loggning. + authentication.setDetails( + new WebAuthenticationDetailsSource().buildDetails(request) + ); + + // 7. Sätt autentiseringen i SecurityContext. + // Från och med nu "vet" Spring Security att det finns en inloggad användare. + // Alla controllers kan hämta den via @AuthenticationPrincipal. + SecurityContextHolder.getContext().setAuthentication(authentication); + + log.debug("Authenticated user email={} via JWT", email); + } + + } catch (JwtException e) { + // Token är ogiltig (felaktig signatur, utgången, korrupt). + // Vi loggar och släpper vidare — Spring Security returnerar 401 automatiskt + // eftersom SecurityContext förblir tom. + log.warn("Invalid JWT token: {}", e.getMessage()); + } + + // Släpp vidare requesten till nästa filter i kedjan (och sedan till controllern) + filterChain.doFilter(request, response); + } +} From 74ca7b60d73cb9f4a1f8c17ce381baa6d34ec881 Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Sat, 11 Apr 2026 18:23:09 +0200 Subject: [PATCH 21/85] feat: configure SecurityConfig for JWT and CORS setup - Added SecurityFilterChain to define security rules, including JWT-based authentication and CORS configuration. - Introduced AuthenticationManager, JwtDecoder, and DaoAuthenticationProvider beans. - Enabled stateless session handling for RESTful API requests. --- .../vet1177/security/SecurityConfig.java | 129 +++++++++++++++++- 1 file changed, 125 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/example/vet1177/security/SecurityConfig.java b/src/main/java/org/example/vet1177/security/SecurityConfig.java index 3f867e90..377048b2 100644 --- a/src/main/java/org/example/vet1177/security/SecurityConfig.java +++ b/src/main/java/org/example/vet1177/security/SecurityConfig.java @@ -1,28 +1,149 @@ package org.example.vet1177.security; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import java.util.List; + +/** + * Hela säkerhetskonfigurationen för applikationen. + * + * @EnableConfigurationProperties(JwtProperties.class) — talar om för Spring att + * läsa jwt.secret-key och jwt.expiration-ms från application.properties och + * skapa en JwtProperties-bean som kan injiceras i JwtService. + */ @Configuration +@EnableConfigurationProperties(JwtProperties.class) public class SecurityConfig { + private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final CustomUserDetailsService userDetailsService; + + public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter, + CustomUserDetailsService userDetailsService) { + this.jwtAuthenticationFilter = jwtAuthenticationFilter; + this.userDetailsService = userDetailsService; + } + + /** + * SecurityFilterChain — definierar hela säkerhetskedjan. + * + * Varje rad i kedjan lägger till en regel: + */ @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) - throws Exception { + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http + // CORS — tillåter frontend (annan port/domän) att anropa vårt API. + // Utan detta blockerar webbläsaren alla cross-origin requests. + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + + // CSRF — avstängt. CSRF-skydd behövs för cookie-baserad auth + // men är irrelevant för JWT (token skickas i header, inte cookie). .csrf(csrf -> csrf.disable()) + + // Sessionhantering — STATELESS. + // Servern sparar ingen session. All info finns i JWT-tokenen. + // Varje request är oberoende — filtret avkodar token varje gång. + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + + // Endpoint-regler — vilka URLs kräver vad. + // Ordningen spelar roll: första matchande regel vinner. .authorizeHttpRequests(auth -> auth - .anyRequest().permitAll() // ← temporärt, öppnar allt + // Öppna endpoints — ingen token krävs + .requestMatchers("/api/auth/**").permitAll() + .requestMatchers(HttpMethod.GET, "/api/clinics", "/api/clinics/**").permitAll() + + // Alla andra API-anrop kräver att man är inloggad + .anyRequest().authenticated() ) + + // Registrera vårt JWT-filter FÖRE Springs inbyggda UsernamePasswordAuthenticationFilter. + // Det betyder att JWT-filtret körs först och sätter SecurityContext + // innan Spring kollar om användaren har rätt behörighet. + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .build(); } + /** + * JwtDecoder-bean — Spring Security använder denna för att verifiera JWT-tokens. + * Vi hämtar den från JwtService som redan har konfigurerat den med vår hemliga nyckel. + */ + @Bean + public JwtDecoder jwtDecoder(JwtService jwtService) { + return jwtService.getJwtDecoder(); + } + + /** + * PasswordEncoder — BCrypt är industristandard. + * Används vid registrering (hasha lösenord) och login (verifiera lösenord). + * BCrypt saltar automatiskt — samma lösenord ger olika hash varje gång. + */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } -} \ No newline at end of file + + /** + * AuthenticationManager — hanterar login-flödet. + * + * När Person B:s AuthController anropar authManager.authenticate(email, password): + * 1. AuthenticationManager delegerar till DaoAuthenticationProvider + * 2. DaoAuthenticationProvider anropar CustomUserDetailsService.loadUserByUsername(email) + * 3. DaoAuthenticationProvider jämför lösenord med PasswordEncoder.matches() + * 4. Om match → returnerar Authentication med User-objektet + * 5. Om mismatch → kastar BadCredentialsException + */ + @Bean + public AuthenticationManager authenticationManager( + AuthenticationConfiguration authConfig) throws Exception { + return authConfig.getAuthenticationManager(); + } + + /** + * DaoAuthenticationProvider — kopplar ihop UserDetailsService med PasswordEncoder. + * "Dao" = Data Access Object — den hämtar data från vår databas. + */ + @Bean + public DaoAuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService); + provider.setPasswordEncoder(passwordEncoder()); + return provider; + } + + /** + * CORS-konfiguration — vilka origins (domäner) som får anropa vårt API. + * + * I utveckling: localhost:5173 (Vite/React), localhost:3000 (Next.js) + * I produktion: byt till er riktiga frontend-URL. + */ + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedOrigins(List.of("http://localhost:5173", "http://localhost:3000")); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + config.setAllowedHeaders(List.of("Authorization", "Content-Type")); + config.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/api/**", config); + return source; + } +} From 8328c2637ba8d7e63bd3d30d298bb8a830422b2a Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Sat, 11 Apr 2026 18:23:29 +0200 Subject: [PATCH 22/85] feat: implement CustomUserDetailsService for user authentication integration - Provides a bridge between Spring Security and the database. - Loads user details by email for JWT-based authentication. - Logs user lookup actions and handles not found scenarios. --- .../security/CustomUserDetailsService.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/main/java/org/example/vet1177/security/CustomUserDetailsService.java diff --git a/src/main/java/org/example/vet1177/security/CustomUserDetailsService.java b/src/main/java/org/example/vet1177/security/CustomUserDetailsService.java new file mode 100644 index 00000000..f13b57d4 --- /dev/null +++ b/src/main/java/org/example/vet1177/security/CustomUserDetailsService.java @@ -0,0 +1,49 @@ +package org.example.vet1177.security; + +import org.example.vet1177.repository.UserRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +/** + * Bryggan mellan Spring Security och vår databas. + * + * Spring Security behöver veta hur man hämtar en användare givet ett "användarnamn". + * I vårt fall är "användarnamnet" email-adressen (det som står i JWT:ns subject-claim). + * + * Flödet: + * JWT-token innehåller sub="sara@vet.se" + * ↓ + * JwtAuthenticationFilter anropar loadUserByUsername("sara@vet.se") + * ↓ + * Vi söker i databasen: userRepository.findByEmail("sara@vet.se") + * ↓ + * Returnerar User-objektet (som implementerar UserDetails) + * ↓ + * Spring Security sätter det i SecurityContext → @AuthenticationPrincipal fungerar + */ +@Service +public class CustomUserDetailsService implements UserDetailsService { + + private static final Logger log = LoggerFactory.getLogger(CustomUserDetailsService.class); + + private final UserRepository userRepository; + + public CustomUserDetailsService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + log.debug("Loading user by email={}", email); + + return userRepository.findByEmail(email) + .orElseThrow(() -> { + log.warn("User not found email={}", email); + return new UsernameNotFoundException("Användare hittades inte: " + email); + }); + } +} From d606938b1a6c97223466d7a2ec25664605d0fb1c Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Sat, 11 Apr 2026 18:25:36 +0200 Subject: [PATCH 23/85] CustomUserDetailsService for user authentication integration - Provides a bridge between Spring Security and the database. - Loads user details by email for JWT-based authentication. - Logs user lookup actions and handles not found scenarios. --- .../org/example/vet1177/security/CustomUserDetailsService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/example/vet1177/security/CustomUserDetailsService.java b/src/main/java/org/example/vet1177/security/CustomUserDetailsService.java index f13b57d4..3251b4ed 100644 --- a/src/main/java/org/example/vet1177/security/CustomUserDetailsService.java +++ b/src/main/java/org/example/vet1177/security/CustomUserDetailsService.java @@ -40,6 +40,7 @@ public CustomUserDetailsService(UserRepository userRepository) { public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { log.debug("Loading user by email={}", email); + return userRepository.findByEmail(email) .orElseThrow(() -> { log.warn("User not found email={}", email); From 85928ae501dbdcc849d23758c52c8e3bf8972d00 Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Sat, 11 Apr 2026 18:29:00 +0200 Subject: [PATCH 24/85] test: add JwtService and CustomUserDetailsService mocks in VetControllerTest --- .../org/example/vet1177/controller/VetControllerTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/test/java/org/example/vet1177/controller/VetControllerTest.java b/src/test/java/org/example/vet1177/controller/VetControllerTest.java index 2ffaa8bc..dc1387ce 100644 --- a/src/test/java/org/example/vet1177/controller/VetControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/VetControllerTest.java @@ -6,6 +6,8 @@ import org.example.vet1177.entities.User; import org.example.vet1177.exception.ForbiddenException; import org.example.vet1177.exception.ResourceNotFoundException; +import org.example.vet1177.security.CustomUserDetailsService; +import org.example.vet1177.security.JwtService; import org.example.vet1177.policy.AdminPolicy; import org.example.vet1177.services.VetService; import org.junit.jupiter.api.Test; @@ -39,6 +41,12 @@ class VetControllerTest { @MockitoBean private AdminPolicy adminPolicy; + @MockitoBean + private JwtService jwtService; + + @MockitoBean + private CustomUserDetailsService customUserDetailsService; + // ========================= // POST /api/vets // ========================= From 201c64ab19c9de42800caeac8035742316668611 Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Sat, 11 Apr 2026 18:29:06 +0200 Subject: [PATCH 25/85] test: add JwtService and CustomUserDetailsService mocks in ActivityLogControllerTest --- .../vet1177/controller/ActivityLogControllerTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/test/java/org/example/vet1177/controller/ActivityLogControllerTest.java b/src/test/java/org/example/vet1177/controller/ActivityLogControllerTest.java index 00b584a3..b6e1e2c7 100644 --- a/src/test/java/org/example/vet1177/controller/ActivityLogControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/ActivityLogControllerTest.java @@ -1,6 +1,8 @@ package org.example.vet1177.controller; import org.example.vet1177.entities.*; +import org.example.vet1177.security.CustomUserDetailsService; +import org.example.vet1177.security.JwtService; import org.example.vet1177.services.ActivityLogService; import org.example.vet1177.services.UserService; import org.junit.jupiter.api.Test; @@ -34,6 +36,12 @@ class ActivityLogControllerTest { @MockitoBean private UserService userService; + @MockitoBean + private JwtService jwtService; + + @MockitoBean + private CustomUserDetailsService customUserDetailsService; + @Test void shouldReturnLogsForRecord() throws Exception { From 67000d189ad669de0dbd94cf6f513b44f9944702 Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Sat, 11 Apr 2026 18:29:11 +0200 Subject: [PATCH 26/85] test: add JwtService and CustomUserDetailsService mocks in ClinicControllerTest --- .../example/vet1177/controller/ClinicControllerTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/test/java/org/example/vet1177/controller/ClinicControllerTest.java b/src/test/java/org/example/vet1177/controller/ClinicControllerTest.java index fe8892f1..c0548091 100644 --- a/src/test/java/org/example/vet1177/controller/ClinicControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/ClinicControllerTest.java @@ -1,6 +1,8 @@ package org.example.vet1177.controller; import org.example.vet1177.entities.Clinic; +import org.example.vet1177.security.CustomUserDetailsService; +import org.example.vet1177.security.JwtService; import org.example.vet1177.services.ClinicService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -26,6 +28,12 @@ class ClinicControllerTest { @MockitoBean private ClinicService clinicService; + @MockitoBean + private JwtService jwtService; + + @MockitoBean + private CustomUserDetailsService customUserDetailsService; + @Test void shouldCreateClinic() throws Exception { // Arrange From 295a52a8079c3e0d70c2b6985eade9b218e394be Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Sat, 11 Apr 2026 18:29:18 +0200 Subject: [PATCH 27/85] test: add JwtService and CustomUserDetailsService mocks in CommentControllerTest --- .../example/vet1177/controller/CommentControllerTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/test/java/org/example/vet1177/controller/CommentControllerTest.java b/src/test/java/org/example/vet1177/controller/CommentControllerTest.java index 37ccdf8b..fe30a6af 100644 --- a/src/test/java/org/example/vet1177/controller/CommentControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/CommentControllerTest.java @@ -15,6 +15,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; import org.springframework.context.annotation.Import; +import org.example.vet1177.security.CustomUserDetailsService; +import org.example.vet1177.security.JwtService; import org.example.vet1177.security.SecurityConfig; import org.springframework.http.MediaType; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -45,6 +47,12 @@ class CommentControllerTest { @MockitoBean private CommentService commentService; + @MockitoBean + private JwtService jwtService; + + @MockitoBean + private CustomUserDetailsService customUserDetailsService; + private User currentUser; private MedicalRecord record; private Comment comment; From aaee79b04f02b69001c422f4dfadcf49a278154d Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Sat, 11 Apr 2026 18:29:29 +0200 Subject: [PATCH 28/85] test: add JwtService and CustomUserDetailsService mocks in MedicalRecordControllerTest --- .../vet1177/controller/MedicalRecordControllerTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/test/java/org/example/vet1177/controller/MedicalRecordControllerTest.java b/src/test/java/org/example/vet1177/controller/MedicalRecordControllerTest.java index 2b7d8a14..38b44129 100644 --- a/src/test/java/org/example/vet1177/controller/MedicalRecordControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/MedicalRecordControllerTest.java @@ -15,6 +15,8 @@ import org.example.vet1177.exception.ForbiddenException; import org.example.vet1177.exception.ResourceNotFoundException; import org.example.vet1177.policy.MedicalRecordPolicy; +import org.example.vet1177.security.CustomUserDetailsService; +import org.example.vet1177.security.JwtService; import org.example.vet1177.security.SecurityConfig; import org.example.vet1177.services.MedicalRecordService; import org.example.vet1177.services.UserService; @@ -59,6 +61,12 @@ class MedicalRecordControllerTest { @MockitoBean private UserService userService; + @MockitoBean + private JwtService jwtService; + + @MockitoBean + private CustomUserDetailsService customUserDetailsService; + private User vetUser; private User ownerUser; private Pet pet; From 07bce62f72ce69aa4c9231c8caaa1d7617e27c59 Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Sat, 11 Apr 2026 18:40:22 +0200 Subject: [PATCH 29/85] test: add authentication setup in ActivityLogIntegrationTest and AttachmentControllerTest - Integrated `UsernamePasswordAuthenticationToken` for authentication in `ActivityLogIntegrationTest`. - Added `JwtService` and `CustomUserDetailsService` mocks in `AttachmentControllerTest`. --- .../controller/AttachmentControllerTest.java | 8 ++++++ .../ActivityLogIntegrationTest.java | 27 +++++++++++++------ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/test/java/org/example/vet1177/controller/AttachmentControllerTest.java b/src/test/java/org/example/vet1177/controller/AttachmentControllerTest.java index 01467030..9fb12fa5 100644 --- a/src/test/java/org/example/vet1177/controller/AttachmentControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/AttachmentControllerTest.java @@ -5,6 +5,8 @@ import org.example.vet1177.entities.User; import org.example.vet1177.exception.ForbiddenException; import org.example.vet1177.exception.ResourceNotFoundException; +import org.example.vet1177.security.CustomUserDetailsService; +import org.example.vet1177.security.JwtService; import org.example.vet1177.security.SecurityConfig; import org.example.vet1177.services.AttachmentService; import org.junit.jupiter.api.BeforeEach; @@ -39,6 +41,12 @@ class AttachmentControllerTest { @MockitoBean private AttachmentService attachmentService; + @MockitoBean + private JwtService jwtService; + + @MockitoBean + private CustomUserDetailsService customUserDetailsService; + private User vetUser; private UUID recordId; private UUID attachmentId; diff --git a/src/test/java/org/example/vet1177/integration/activitylog/ActivityLogIntegrationTest.java b/src/test/java/org/example/vet1177/integration/activitylog/ActivityLogIntegrationTest.java index 385562fd..9e1d587f 100644 --- a/src/test/java/org/example/vet1177/integration/activitylog/ActivityLogIntegrationTest.java +++ b/src/test/java/org/example/vet1177/integration/activitylog/ActivityLogIntegrationTest.java @@ -1,6 +1,5 @@ package org.example.vet1177.integration.activitylog; -import org.checkerframework.checker.units.qual.C; import org.example.vet1177.config.AwsS3Properties; import org.example.vet1177.entities.*; import org.example.vet1177.integration.TestDataFactory; @@ -10,15 +9,15 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; -import java.math.BigDecimal; -import java.time.LocalDate; import java.util.UUID; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -137,7 +136,10 @@ void should_return_logs_for_owner_only() throws Exception { // Act & Assert mockMvc.perform(get("/api/activity-logs/record/" + record.getId()) - .header("userId", owner.getId().toString())) + .header("userId", owner.getId().toString()) + .with(authentication(new UsernamePasswordAuthenticationToken( + owner, null, owner.getAuthorities() + )))) .andExpect(status().isOk()) .andExpect(jsonPath("$.length()").value(2)); } @@ -166,7 +168,10 @@ void should_allow_vet_in_same_clinic_to_see_logs() throws Exception { ActivityType.CASE_CREATED, "log"); mockMvc.perform(get("/api/activity-logs/record/" + record.getId()) - .header("userId", vet.getId().toString())) + .header("userId", vet.getId().toString()) + .with(authentication(new UsernamePasswordAuthenticationToken( + vet, null, vet.getAuthorities() + )))) .andExpect(status().isOk()) .andExpect(jsonPath("$.length()").value(1)); } @@ -197,15 +202,21 @@ void should_filter_out_logs_for_vet_in_other_clinic() throws Exception { ActivityType.CASE_CREATED, "log"); mockMvc.perform(get("/api/activity-logs/record/" + record.getId()) - .header("userId", vetOtherClinic.getId().toString())) + .header("userId", vetOtherClinic.getId().toString()) + .with(authentication(new UsernamePasswordAuthenticationToken( + vetOtherClinic, null, vetOtherClinic.getAuthorities() + )))) .andExpect(status().isOk()) .andExpect(jsonPath("$.length()").value(0)); } @Test void should_return_400_if_userId_missing() throws Exception { - - mockMvc.perform(get("/api/activity-logs/record/" + UUID.randomUUID())) + User anyUser = new User("Test", "test@test.com", "hash", Role.VET); + mockMvc.perform(get("/api/activity-logs/record/" + UUID.randomUUID()) + .with(authentication(new UsernamePasswordAuthenticationToken( + anyUser, null, anyUser.getAuthorities() + )))) .andExpect(status().isBadRequest()); } } From 69ce4934d044cbebefa85313532964901bf43d61 Mon Sep 17 00:00:00 2001 From: Tatjana Date: Sun, 12 Apr 2026 22:23:47 +0200 Subject: [PATCH 30/85] Test: integration tests for vet --- .../integration/vet/VetIntegrationTest.java | 302 ++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 src/test/java/org/example/vet1177/integration/vet/VetIntegrationTest.java diff --git a/src/test/java/org/example/vet1177/integration/vet/VetIntegrationTest.java b/src/test/java/org/example/vet1177/integration/vet/VetIntegrationTest.java new file mode 100644 index 00000000..bda03995 --- /dev/null +++ b/src/test/java/org/example/vet1177/integration/vet/VetIntegrationTest.java @@ -0,0 +1,302 @@ +package org.example.vet1177.integration.vet; + +import org.example.vet1177.config.AwsS3Properties; +import org.example.vet1177.entities.Clinic; +import org.example.vet1177.entities.Role; +import org.example.vet1177.entities.User; +import org.example.vet1177.entities.Vet; +import org.example.vet1177.exception.ForbiddenException; +import org.example.vet1177.integration.TestDataFactory; +import org.example.vet1177.policy.AdminPolicy; +import org.example.vet1177.repository.ClinicRepository; +import org.example.vet1177.repository.UserRepository; +import org.example.vet1177.repository.VetRepository; +import org.example.vet1177.services.FileStorageService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc(addFilters = false) +@ActiveProfiles("test") +@TestPropertySource(properties = { + "spring.datasource.url=jdbc:h2:mem:testdb;MODE=PostgreSQL", + "spring.datasource.driver-class-name=org.h2.Driver" +}) +class VetIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private VetRepository vetRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ClinicRepository clinicRepository; + + @MockitoBean + private FileStorageService fileStorageService; + + @MockitoBean + private AwsS3Properties awsS3Properties; + + @MockitoBean + private AdminPolicy adminPolicy; + + @BeforeEach + void setUp() { + vetRepository.deleteAll(); + userRepository.deleteAll(); + clinicRepository.deleteAll(); + } + + @BeforeEach + void resetMocks() { + reset(adminPolicy); + } + + private User createAdmin(Clinic clinic) { + User admin = new User( + "Admin", + UUID.randomUUID() + "@test.com", + "password123", + Role.ADMIN, + clinic + ); + return userRepository.save(admin); + } + + private User createVetUser(Clinic clinic) { + User vetUser = new User( + "Vet User", + UUID.randomUUID() + "@test.com", + "password123", + Role.VET, + clinic + ); + return userRepository.save(vetUser); + } + + private UsernamePasswordAuthenticationToken auth(User user) { + return new UsernamePasswordAuthenticationToken(user, null, List.of()); + } + + @Test + void VT_P0_01_admin_can_create_vet() throws Exception { + doNothing().when(adminPolicy).requireAdmin(any()); + Clinic clinic = TestDataFactory.createClinic(clinicRepository); + User admin = createAdmin(clinic); + User targetUser = TestDataFactory.createOwner(userRepository, clinic); + + String body = """ + { + "userId": "%s", + "licenseId": "LIC-1001", + "specialization": "Surgery", + "bookingInfo": "Weekdays only" + } + """.formatted(targetUser.getId()); + + mockMvc.perform(post("/api/vets") + .with(authentication(auth(admin))) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.userId").value(targetUser.getId().toString())) + .andExpect(jsonPath("$.licenseId").value("LIC-1001")) + .andExpect(jsonPath("$.specialization").value("Surgery")) + .andExpect(jsonPath("$.bookingInfo").value("Weekdays only")); + + assertEquals(1, vetRepository.count()); + + User updatedUser = userRepository.findById(targetUser.getId()).orElseThrow(); + assertEquals(Role.VET, updatedUser.getRole()); + } + // TODO: Enable when real authentication is implemented +// @Test +// void VT_P0_02_non_admin_cannot_create_vet_as_vet() throws Exception { +// Clinic clinic = TestDataFactory.createClinic(clinicRepository); +// User nonAdminVet = createVetUser(clinic); +// User targetUser = TestDataFactory.createOwner(userRepository, clinic); +// +// String body = """ +// { +// "userId": "%s", +// "licenseId": "LIC-1003", +// "specialization": "Cardiology", +// "bookingInfo": "Tue-Thu" +// } +// """.formatted(targetUser.getId()); +// +// mockMvc.perform(post("/api/vets") +// .with(authentication(auth(nonAdminVet))) +// .contentType(MediaType.APPLICATION_JSON) +// .content(body)) +// .andExpect(status().isForbidden()); +// +// assertEquals(0, vetRepository.count()); +// } + + @Test + void VT_P0_03_duplicate_license_id_rejected() throws Exception { + Clinic clinic = TestDataFactory.createClinic(clinicRepository); + User admin = createAdmin(clinic); + + User firstUser = TestDataFactory.createOwner(userRepository, clinic); + User secondUser = TestDataFactory.createOwner(userRepository, clinic); + + Vet existingVet = new Vet(firstUser, "LIC-DUP-1", "Surgery", "Morning"); + vetRepository.save(existingVet); + + String body = """ + { + "userId": "%s", + "licenseId": "LIC-DUP-1", + "specialization": "Dentistry", + "bookingInfo": "Afternoon" + } + """.formatted(secondUser.getId()); + + mockMvc.perform(post("/api/vets") + .with(authentication(auth(admin))) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isBadRequest()); + + assertEquals(1, vetRepository.count()); + assertTrue(vetRepository.existsByLicenseId("LIC-DUP-1")); + } + + @Test + void VT_P0_04_missing_target_user_rejected() throws Exception { + Clinic clinic = TestDataFactory.createClinic(clinicRepository); + User admin = createAdmin(clinic); + + String body = """ + { + "userId": "%s", + "licenseId": "LIC-404", + "specialization": "Neurology", + "bookingInfo": "By appointment" + } + """.formatted(UUID.randomUUID()); + + mockMvc.perform(post("/api/vets") + .with(authentication(auth(admin))) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isNotFound()); + + assertEquals(0, vetRepository.count()); + } + + @Test + void VT_P0_05_get_all_vets_includes_clinic_name() throws Exception { + Clinic clinic = TestDataFactory.createClinic(clinicRepository); + User user = createVetUser(clinic); + + Vet vet = new Vet(user, "LIC-GET-ALL", "Orthopedics", "Mon-Wed"); + vetRepository.save(vet); + + mockMvc.perform(get("/api/vets")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].userId").value(user.getId().toString())) + .andExpect(jsonPath("$[0].licenseId").value("LIC-GET-ALL")) + .andExpect(jsonPath("$[0].clinicName").value(clinic.getName())); + } + + @Test + void VT_P0_06_get_vet_by_id_returns_expected_vet() throws Exception { + Clinic clinic = TestDataFactory.createClinic(clinicRepository); + User user = createVetUser(clinic); + + Vet vet = new Vet(user, "LIC-BY-ID", "Internal medicine", "Fri"); + vetRepository.save(vet); + + mockMvc.perform(get("/api/vets/" + user.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.userId").value(user.getId().toString())) + .andExpect(jsonPath("$.licenseId").value("LIC-BY-ID")) + .andExpect(jsonPath("$.clinicName").value(clinic.getName())); + } + + @Test + void VT_P0_06_get_vet_by_id_returns_404_for_unknown_id() throws Exception { + mockMvc.perform(get("/api/vets/" + UUID.randomUUID())) + .andExpect(status().isNotFound()); + } + + @Test + void VT_P1_01_validation_error_for_invalid_vet_request() throws Exception { + Clinic clinic = TestDataFactory.createClinic(clinicRepository); + User admin = createAdmin(clinic); + + String body = """ + { + "userId": null, + "licenseId": "", + "specialization": "Valid specialization", + "bookingInfo": "Valid booking info" + } + """; + + mockMvc.perform(post("/api/vets") + .with(authentication(auth(admin))) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isBadRequest()); + + assertEquals(0, vetRepository.count()); + } + + @Test + void VT_P1_02_existing_vet_role_remains_stable() throws Exception { + Clinic clinic = TestDataFactory.createClinic(clinicRepository); + User admin = createAdmin(clinic); + User targetUser = createVetUser(clinic); // redan VET, men ännu ingen vet_details-rad + + String body = """ + { + "userId": "%s", + "licenseId": "LIC-STABLE", + "specialization": "Exotics", + "bookingInfo": "Weekends" + } + """.formatted(targetUser.getId()); + + mockMvc.perform(post("/api/vets") + .with(authentication(auth(admin))) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.userId").value(targetUser.getId().toString())) + .andExpect(jsonPath("$.licenseId").value("LIC-STABLE")); + + User reloadedUser = userRepository.findById(targetUser.getId()).orElseThrow(); + assertEquals(Role.VET, reloadedUser.getRole()); + assertEquals(1, vetRepository.count()); + } +} \ No newline at end of file From 4c68a680abf256ae5556b8bb365ffdd9d2d87c0e Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Mon, 13 Apr 2026 09:10:55 +0200 Subject: [PATCH 31/85] test: DELETE /pets/{petId} --- .../vet1177/controller/PetControllerTest.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/test/java/org/example/vet1177/controller/PetControllerTest.java b/src/test/java/org/example/vet1177/controller/PetControllerTest.java index 8f1488f7..199311b2 100644 --- a/src/test/java/org/example/vet1177/controller/PetControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/PetControllerTest.java @@ -216,5 +216,42 @@ void updatePet_whenForbidden_shouldReturn403() throws Exception { .andExpect(status().isForbidden()); } + //DELETE /pets/{petId} + + + @Test + void deletePet_shouldReturn204() throws Exception { + when(userService.getUserEntityById(any())).thenReturn(owner); + doNothing().when(petService).deletePet(any(), any()); + + mockMvc.perform(delete("/pets/{petId}", petId) + .with(authenticatedAs(owner)) + .header("currentUserId", ownerId)) + .andExpect(status().isNoContent()); + } + + @Test + void deletePet_whenNotFound_shouldReturn404() throws Exception { + when(userService.getUserEntityById(any())).thenReturn(owner); + doThrow(new ResourceNotFoundException("Pet", petId)) + .when(petService).deletePet(any(), any()); + + mockMvc.perform(delete("/pets/{petId}", petId) + .with(authenticatedAs(owner)) + .header("currentUserId", ownerId)) + .andExpect(status().isNotFound()); + } + + @Test + void deletePet_whenForbidden_shouldReturn403() throws Exception { + when(userService.getUserEntityById(any())).thenReturn(vet); + doThrow(new ForbiddenException("Du har inte behörighet att radera detta djur")) + .when(petService).deletePet(any(), any()); + + mockMvc.perform(delete("/pets/{petId}", petId) + .with(authenticatedAs(vet)) + .header("currentUserId", UUID.randomUUID())) + .andExpect(status().isForbidden()); + } } From a401a6b33bfc69523adf5aa3b9c3ac4f1b557c1a Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Mon, 13 Apr 2026 11:02:21 +0200 Subject: [PATCH 32/85] rabbit fixes --- .../controller/CommentControllerTest.java | 1 + .../vet1177/controller/PetControllerTest.java | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/example/vet1177/controller/CommentControllerTest.java b/src/test/java/org/example/vet1177/controller/CommentControllerTest.java index 37ccdf8b..99806aa9 100644 --- a/src/test/java/org/example/vet1177/controller/CommentControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/CommentControllerTest.java @@ -22,6 +22,7 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.RequestPostProcessor; + import java.util.List; import java.util.UUID; diff --git a/src/test/java/org/example/vet1177/controller/PetControllerTest.java b/src/test/java/org/example/vet1177/controller/PetControllerTest.java index 199311b2..2564605e 100644 --- a/src/test/java/org/example/vet1177/controller/PetControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/PetControllerTest.java @@ -1,6 +1,8 @@ package org.example.vet1177.controller; -import org.example.vet1177.exception.ResourceNotFoundException; + +//Används i commentControllerTest också, verkar funka där? import tools.jackson.databind.ObjectMapper; +import org.example.vet1177.exception.ResourceNotFoundException; import org.example.vet1177.dto.request.pet.PetRequest; import org.example.vet1177.entities.Pet; import org.example.vet1177.entities.Role; @@ -113,6 +115,19 @@ void createPet_whenForbidden_shouldReturn403() throws Exception { .content(objectMapper.writeValueAsString(validPetRequest()))) .andExpect(status().isForbidden()); } + + @Test + void createPet_whenUnexpectedError_shouldReturn500() throws Exception { + when(petService.createPet(any(), any(), any())) + .thenThrow(new RuntimeException("Unexpected failure")); + + mockMvc.perform(post("/pets") + .with(authenticatedAs(owner)) + .header("currentUserId", ownerId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(validPetRequest()))) + .andExpect(status().isInternalServerError()); + } // GET /pets/{petId} @Test From 289fbd16f56bfef1f2884f6d1b2f142dfe149a1a Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Mon, 13 Apr 2026 16:19:21 +0200 Subject: [PATCH 33/85] fix: specify UTF-8 charset for secret key generation in JwtService --- src/main/java/org/example/vet1177/security/JwtService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/example/vet1177/security/JwtService.java b/src/main/java/org/example/vet1177/security/JwtService.java index cbb3233a..025be968 100644 --- a/src/main/java/org/example/vet1177/security/JwtService.java +++ b/src/main/java/org/example/vet1177/security/JwtService.java @@ -10,6 +10,7 @@ import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; import java.time.Instant; /** @@ -39,7 +40,7 @@ public JwtService(JwtProperties jwtProperties) { // SecretKeySpec tar emot byte-arrayen + algoritmen och ger oss ett SecretKey-objekt // som Java:s krypto-API kan använda. SecretKey key = new SecretKeySpec( - jwtProperties.getSecretKey().getBytes(), + jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8), "HmacSHA256" ); From 184fbbe4b724ca212c1f9344bed1e05d03c23182 Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Mon, 13 Apr 2026 16:20:05 +0200 Subject: [PATCH 34/85] fix: handle UsernameNotFoundException in JwtAuthenticationFilter - Log a warning when a valid JWT token references a non-existent user. --- .../example/vet1177/security/JwtAuthenticationFilter.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/org/example/vet1177/security/JwtAuthenticationFilter.java b/src/main/java/org/example/vet1177/security/JwtAuthenticationFilter.java index c0d3b3df..519eb079 100644 --- a/src/main/java/org/example/vet1177/security/JwtAuthenticationFilter.java +++ b/src/main/java/org/example/vet1177/security/JwtAuthenticationFilter.java @@ -9,6 +9,7 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.oauth2.jwt.JwtException; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; @@ -115,6 +116,10 @@ protected void doFilterInternal( // Vi loggar och släpper vidare — Spring Security returnerar 401 automatiskt // eftersom SecurityContext förblir tom. log.warn("Invalid JWT token: {}", e.getMessage()); + } catch (UsernameNotFoundException e) { + // Token är giltig men användaren finns inte längre i databasen + // (t.ex. raderad av admin medan tokenen fortfarande gäller). + log.warn("User from JWT no longer exists: {}", e.getMessage()); } // Släpp vidare requesten till nästa filter i kedjan (och sedan till controllern) From bc1a131b24521a0e5c5d57d057fbf31a86a5f40a Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Mon, 13 Apr 2026 16:32:41 +0200 Subject: [PATCH 35/85] fix: validate secret key length for HS256 in JwtService - Ensure JWT secret key is at least 32 bytes to comply with HS256 requirements. - Update property name to `jwt.secret-key` for clarity in application-test.properties. Closes #61 --- .../org/example/vet1177/security/JwtService.java | 14 +++++++------- src/test/resources/application-test.properties | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/example/vet1177/security/JwtService.java b/src/main/java/org/example/vet1177/security/JwtService.java index 025be968..6643c970 100644 --- a/src/main/java/org/example/vet1177/security/JwtService.java +++ b/src/main/java/org/example/vet1177/security/JwtService.java @@ -36,13 +36,13 @@ public class JwtService { private final long expirationMs; public JwtService(JwtProperties jwtProperties) { - // Skapar en kryptografisk nyckel från vår hemliga sträng i application.properties. - // SecretKeySpec tar emot byte-arrayen + algoritmen och ger oss ett SecretKey-objekt - // som Java:s krypto-API kan använda. - SecretKey key = new SecretKeySpec( - jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8), - "HmacSHA256" - ); + byte[] keyBytes = jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8); + if (keyBytes.length < 32) { + throw new IllegalStateException( + "JWT secret key must be at least 32 bytes for HS256, got " + keyBytes.length); + } + + SecretKey key = new SecretKeySpec(keyBytes, "HmacSHA256"); // JwtEncoder — skapar nya tokens. // ImmutableSecret wrapprar vår nyckel så att Nimbus-biblioteket kan använda den. diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties index 88e85bcc..4f351edc 100644 --- a/src/test/resources/application-test.properties +++ b/src/test/resources/application-test.properties @@ -1,6 +1,6 @@ # ============================================ -# TEST-profil ? krs av @ActiveProfiles("test") -# Anvnds av integrationstester med @SpringBootTest +# TEST-profil ? k�rs av @ActiveProfiles("test") +# Anv�nds av integrationstester med @SpringBootTest # ============================================ # Separat testdatabas ? aldrig dev-databasen @@ -13,14 +13,14 @@ spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.datasource.hikari.driver-class-name=org.h2.Driver -# Skapa schema frn scratch och rensa efter tester +# Skapa schema fr�n scratch och rensa efter tester spring.jpa.hibernate.ddl-auto=create-drop # Hibernate creates schema automatically (schema.sql is NOT used in tests) spring.sql.init.mode=never spring.sql.init.data-locations= -# Stng av Docker Compose i tester +# St�ng av Docker Compose i tester # CI hanterar databasen som service container spring.docker.compose.lifecycle-management=none @@ -31,8 +31,8 @@ aws.s3.secret-key=minioadmin aws.s3.bucket-name=vet1177-test-attachments aws.s3.region=eu-north-1 -# JWT ? dummyvrden fr tester -jwt.secret=test-secret-key-minst-32-tecken-lang-for-tester +# JWT ? dummyv�rden f�r tester +jwt.secret-key=test-secret-key-minst-32-tecken-lang-for-tester jwt.expiration-ms=86400000 # Mindre output i tester From 1d19d757ae5463c05c4dc13cd674b7bc612a6fa2 Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Tue, 14 Apr 2026 10:05:31 +0200 Subject: [PATCH 36/85] refactor: replace @RequestHeader with @AuthenticationPrincipal in PetController - Simplify user context handling by using `@AuthenticationPrincipal` instead of `@RequestHeader`. - Remove unused `UserService` dependency. - Improve logging for pet-related endpoints. --- .../vet1177/controller/PetController.java | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/example/vet1177/controller/PetController.java b/src/main/java/org/example/vet1177/controller/PetController.java index 0c808159..99677543 100644 --- a/src/main/java/org/example/vet1177/controller/PetController.java +++ b/src/main/java/org/example/vet1177/controller/PetController.java @@ -8,8 +8,8 @@ import org.example.vet1177.services.PetService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.example.vet1177.services.UserService; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -22,44 +22,42 @@ public class PetController { private static final Logger log = LoggerFactory.getLogger(PetController.class); private final PetService petService; - private final UserService userService; - public PetController(PetService petService, UserService userService) { + public PetController(PetService petService) { this.petService = petService; - this.userService = userService; } - //POST / pets - skapa nytt djur + //POST /pets - skapa nytt djur @PostMapping public PetResponse createPet( - // TODO: Ersätt med användare från autentiserad kontext (t.ex. JWT / Spring Security) - @RequestHeader UUID currentUserId, + @AuthenticationPrincipal User currentUser, @RequestParam(required = false) UUID ownerId, @Valid @RequestBody PetRequest request ) { - log.info("POST /pets - creating pet for currentUserId={} ownerId={}", currentUserId, ownerId); - Pet saved = petService.createPet(currentUserId, ownerId, request); + log.info("POST /pets - creating pet for userId={} ownerId={}", currentUser.getId(), ownerId); + Pet saved = petService.createPet(currentUser.getId(), ownerId, request); return toResponse(saved); } - // GET / pets/{petId} - hämta ett specifikt djur + // GET /pets/{petId} - hämta ett specifikt djur @GetMapping("/{petId}") public ResponseEntity getPetById( - @RequestHeader UUID currentUserId, + @AuthenticationPrincipal User currentUser, @PathVariable UUID petId ) { - User currentUser = userService.getUserEntityById(currentUserId); + log.info("GET /pets/{}", petId); Pet pet = petService.getPetById(petId, currentUser); return ResponseEntity.ok(toResponse(pet)); } - // GET/pets/owner/{ownerId} - hämta alla djur för en ägare + // GET /pets/owner/{ownerId} - hämta alla djur för en ägare @GetMapping("/owner/{ownerId}") public ResponseEntity> getPetsByOwner( - @RequestHeader UUID currentUserId, + @AuthenticationPrincipal User currentUser, @PathVariable UUID ownerId ) { - List pets = petService.getPetsByOwner(currentUserId, ownerId) + log.info("GET /pets/owner/{}", ownerId); + List pets = petService.getPetsByOwner(currentUser.getId(), ownerId) .stream() .map(this::toResponse) .toList(); @@ -69,21 +67,22 @@ public ResponseEntity> getPetsByOwner( // PUT /pets/{petId} - uppdatera ett djur @PutMapping("/{petId}") public ResponseEntity updatePet( - @RequestHeader UUID currentUserId, + @AuthenticationPrincipal User currentUser, @PathVariable UUID petId, @Valid @RequestBody PetRequest request ) { - Pet updated = petService.updatePet(currentUserId, petId, request); + log.info("PUT /pets/{}", petId); + Pet updated = petService.updatePet(currentUser.getId(), petId, request); return ResponseEntity.ok(toResponse(updated)); } // DELETE /pets/{petId} - radera ett djur @DeleteMapping("/{petId}") public ResponseEntity deletePet( - @RequestHeader UUID currentUserId, + @AuthenticationPrincipal User currentUser, @PathVariable UUID petId ) { - User currentUser = userService.getUserEntityById(currentUserId); + log.info("DELETE /pets/{}", petId); petService.deletePet(petId, currentUser); return ResponseEntity.noContent().build(); } @@ -103,4 +102,4 @@ private PetResponse toResponse(Pet pet) { ); } -} \ No newline at end of file +} From a7d1f66eb507bfae61a28c364835da18e6ced163 Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Tue, 14 Apr 2026 10:05:38 +0200 Subject: [PATCH 37/85] refactor: use @AuthenticationPrincipal in ActivityLogController - Replace `@RequestHeader` with `@AuthenticationPrincipal` for user context. - Remove unused `UserService` dependency. --- .../vet1177/controller/ActivityLogController.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/example/vet1177/controller/ActivityLogController.java b/src/main/java/org/example/vet1177/controller/ActivityLogController.java index 5086b488..898dc1b8 100644 --- a/src/main/java/org/example/vet1177/controller/ActivityLogController.java +++ b/src/main/java/org/example/vet1177/controller/ActivityLogController.java @@ -3,9 +3,9 @@ import org.example.vet1177.dto.response.activitylog.ActivityLogResponse; import org.example.vet1177.entities.User; import org.example.vet1177.services.ActivityLogService; -import org.example.vet1177.services.UserService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -18,26 +18,22 @@ public class ActivityLogController { private static final Logger log = LoggerFactory.getLogger(ActivityLogController.class); private final ActivityLogService activityLogService; - private final UserService userService; - public ActivityLogController(ActivityLogService activityLogService, - UserService userService) { + public ActivityLogController(ActivityLogService activityLogService) { this.activityLogService = activityLogService; - this.userService = userService; } // Hämta alla logs för ett MedicalRecord @GetMapping("/record/{recordId}") public List getLogsByRecord( @PathVariable UUID recordId, - @RequestHeader("userId") UUID userId + @AuthenticationPrincipal User currentUser ) { log.info("GET /api/activity-logs/record/{}", recordId); - User user = userService.getUserEntityById(userId); - return activityLogService.getByRecord(recordId, user) + return activityLogService.getByRecord(recordId, currentUser) .stream() .map(ActivityLogResponse::from) .toList(); } -} \ No newline at end of file +} From c7ae70ed4e62ce3242e11b2d9e74e69c26d9461e Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Tue, 14 Apr 2026 10:05:49 +0200 Subject: [PATCH 38/85] test: update ActivityLogControllerTest for Spring Security setup - Added `UsernamePasswordAuthenticationToken` for mock authentication. - Imported `SecurityConfig` to configure security for tests. - Removed unused `UserService` mock. --- .../controller/ActivityLogControllerTest.java | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/test/java/org/example/vet1177/controller/ActivityLogControllerTest.java b/src/test/java/org/example/vet1177/controller/ActivityLogControllerTest.java index b6e1e2c7..aa92f78b 100644 --- a/src/test/java/org/example/vet1177/controller/ActivityLogControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/ActivityLogControllerTest.java @@ -3,15 +3,13 @@ import org.example.vet1177.entities.*; import org.example.vet1177.security.CustomUserDetailsService; import org.example.vet1177.security.JwtService; +import org.example.vet1177.security.SecurityConfig; import org.example.vet1177.services.ActivityLogService; -import org.example.vet1177.services.UserService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; - -// ✅ RÄTT (Boot 4) import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; - +import org.springframework.context.annotation.Import; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; @@ -20,11 +18,12 @@ import java.util.UUID; import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @WebMvcTest(ActivityLogController.class) -@AutoConfigureMockMvc(addFilters = false) // 🔥 stänger av security (fixar 401) +@Import(SecurityConfig.class) class ActivityLogControllerTest { @Autowired @@ -33,9 +32,6 @@ class ActivityLogControllerTest { @MockitoBean private ActivityLogService activityLogService; - @MockitoBean - private UserService userService; - @MockitoBean private JwtService jwtService; @@ -55,6 +51,7 @@ void shouldReturnLogsForRecord() throws Exception { User user = mock(User.class); when(user.getId()).thenReturn(userId); when(user.getName()).thenReturn("Alice"); + when(user.getAuthorities()).thenReturn(List.of()); // Mock MedicalRecord MedicalRecord record = mock(MedicalRecord.class); @@ -69,17 +66,18 @@ void shouldReturnLogsForRecord() throws Exception { when(log.getMedicalRecord()).thenReturn(record); when(log.getCreatedAt()).thenReturn(createdAt); - // Mock service responses - when(userService.getUserEntityById(userId)).thenReturn(user); + // Mock service when(activityLogService.getByRecord(recordId, user)) .thenReturn(List.of(log)); // Act + Assert mockMvc.perform(get("/api/activity-logs/record/" + recordId) - .header("userId", userId.toString())) + .with(authentication(new UsernamePasswordAuthenticationToken( + user, null, user.getAuthorities() + )))) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].description").value("Created case")) .andExpect(jsonPath("$[0].action").value("CASE_CREATED")) .andExpect(jsonPath("$[0].performedByName").value("Alice")); } -} \ No newline at end of file +} From 8e1f665d4f355ea41abbdcabfa3a6b224b5adcca Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Tue, 14 Apr 2026 10:05:59 +0200 Subject: [PATCH 39/85] test: remove redundant userId headers in ActivityLogIntegrationTest - Removed redundant `userId` headers from test requests. - Deleted test case for missing `userId` header as it is no longer applicable. --- .../activitylog/ActivityLogIntegrationTest.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/test/java/org/example/vet1177/integration/activitylog/ActivityLogIntegrationTest.java b/src/test/java/org/example/vet1177/integration/activitylog/ActivityLogIntegrationTest.java index 9e1d587f..7eeb8858 100644 --- a/src/test/java/org/example/vet1177/integration/activitylog/ActivityLogIntegrationTest.java +++ b/src/test/java/org/example/vet1177/integration/activitylog/ActivityLogIntegrationTest.java @@ -136,7 +136,6 @@ void should_return_logs_for_owner_only() throws Exception { // Act & Assert mockMvc.perform(get("/api/activity-logs/record/" + record.getId()) - .header("userId", owner.getId().toString()) .with(authentication(new UsernamePasswordAuthenticationToken( owner, null, owner.getAuthorities() )))) @@ -168,7 +167,6 @@ void should_allow_vet_in_same_clinic_to_see_logs() throws Exception { ActivityType.CASE_CREATED, "log"); mockMvc.perform(get("/api/activity-logs/record/" + record.getId()) - .header("userId", vet.getId().toString()) .with(authentication(new UsernamePasswordAuthenticationToken( vet, null, vet.getAuthorities() )))) @@ -202,7 +200,6 @@ void should_filter_out_logs_for_vet_in_other_clinic() throws Exception { ActivityType.CASE_CREATED, "log"); mockMvc.perform(get("/api/activity-logs/record/" + record.getId()) - .header("userId", vetOtherClinic.getId().toString()) .with(authentication(new UsernamePasswordAuthenticationToken( vetOtherClinic, null, vetOtherClinic.getAuthorities() )))) @@ -210,13 +207,4 @@ void should_filter_out_logs_for_vet_in_other_clinic() throws Exception { .andExpect(jsonPath("$.length()").value(0)); } - @Test - void should_return_400_if_userId_missing() throws Exception { - User anyUser = new User("Test", "test@test.com", "hash", Role.VET); - mockMvc.perform(get("/api/activity-logs/record/" + UUID.randomUUID()) - .with(authentication(new UsernamePasswordAuthenticationToken( - anyUser, null, anyUser.getAuthorities() - )))) - .andExpect(status().isBadRequest()); - } } From 7865e17811dba62f0721a997f050828ab290bbd7 Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Tue, 14 Apr 2026 10:06:20 +0200 Subject: [PATCH 40/85] docs: add comprehensive API usage guide for frontend developers - Created `API.md` covering detailed usage of backend API. - Included authentication methods, example requests, and responses. - Documented endpoints for pets, medical records, clinics, and comments. - Provided test data and instructions for local development. --- API.md | 812 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 812 insertions(+) create mode 100644 API.md diff --git a/API.md b/API.md new file mode 100644 index 00000000..fd5ff055 --- /dev/null +++ b/API.md @@ -0,0 +1,812 @@ +# Vet1177 API — Guide for frontend-utvecklare + +Denna guide visar hur du anropar backend-API:et från en React + Tailwind CSS-applikation. +Alla exempel är skrivna för en djurägare (OWNER) som heter Anna. + +--- + +## 1. Kom igang + +### Starta backend + +Backend startas i IntelliJ. Se till att Docker Desktop kor (behövs for databasen). +Kör `Vet1177Application.java` — servern startar pa `http://localhost:8080`. + +### Inloggning under utveckling + +Just nu anvander vi en speciell header istället for riktigt login. +Lagg till headern `X-Dev-User` med en e-postadress i varje request. +Det simulerar att du ar inloggad som den anvandaren. + +**OBS:** Detta fungerar bara nar backend kör med dev-profilen (vilket ar default lokalt). + +### Testanvandare + +| Roll | Namn | Email | +|-------|-----------------|-----------------| +| OWNER | Anna Svensson | anna@test.se | +| VET | Erik Veterinär | erik@klinik.se | +| ADMIN | Sara Admin | sara@admin.se | + +Alla testanvandare har lösenordet `password` (hashat i databasen). + +--- + +## 2. Hur man anropar API:et fran React + +### Grundmönster + +Alla anrop följer samma mönster: + +1. Anropa `fetch()` med rätt URL och headers +2. Kolla att `response.ok` ar `true` +3. Läs JSON-datan med `response.json()` + +```javascript +// Grundmönster for att hamta data +const hämtaData = async () => { + const response = await fetch('http://localhost:8080/api/ENDPOINT', { + headers: { + 'X-Dev-User': 'anna@test.se' // Talar om vem du ar + } + }); + + if (!response.ok) { + throw new Error('Något gick fel: ' + response.status); + } + + const data = await response.json(); + return data; +}; +``` + +```javascript +// Grundmönster for att skicka data (POST/PUT) +const skickaData = async (body) => { + const response = await fetch('http://localhost:8080/api/ENDPOINT', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', // Talar om att vi skickar JSON + 'X-Dev-User': 'anna@test.se' + }, + body: JSON.stringify(body) // Gör om JavaScript-objekt till JSON-text + }); + + if (!response.ok) { + throw new Error('Något gick fel: ' + response.status); + } + + const data = await response.json(); + return data; +}; +``` + +--- + +## 3. Endpoints for djurägare (OWNER) + +### 3.1 Hamta alla kliniker + +Anvands for att visa en dropdown nar djurägaren skapar ett nytt arende. +Alla kliniker ar publika — ingen header behövs. + +**`GET /api/clinics`** + +```javascript +const getKliniker = async () => { + const response = await fetch('http://localhost:8080/api/clinics'); + const data = await response.json(); + return data; +}; +``` + +**Svar (lista med kliniker):** + +```json +[ + { + "id": "a1b2c3d4-e5f6-4a5b-8c9d-e0f1a2b3c4d5", + "name": "Djurkliniken Centrum", + "address": "Storgatan 1, Stockholm", + "phoneNumber": "08-123456" + } +] +``` + +--- + +### 3.2 Hamta mina djur + +Visar alla djur som tillhör den inloggade djurägaren. + +**`GET /pets/owner/{ownerId}`** + +```javascript +const getMinaDjur = async () => { + const ownerId = 'c3d4e5f6-a7b8-4c5d-0e1f-a2b3c4d5e6f7'; // Annas ID + + const response = await fetch(`http://localhost:8080/pets/owner/${ownerId}`, { + headers: { + 'X-Dev-User': 'anna@test.se' + } + }); + const data = await response.json(); + return data; +}; +``` + +**Svar (lista med djur):** + +```json +[ + { + "id": "f6a7b8c9-d0e1-4f5a-3b4c-d5e6f7a8b9c0", + "ownerId": "c3d4e5f6-a7b8-4c5d-0e1f-a2b3c4d5e6f7", + "name": "Fido", + "species": "Hund", + "breed": "Labrador", + "dateOfBirth": "2020-01-15", + "weightKg": 25.5, + "createdAt": "2025-01-01T10:00:00Z", + "updatedAt": "2025-01-01T10:00:00Z" + }, + { + "id": "a7b8c9d0-e1f2-4a5b-4c5d-e6f7a8b9c0d1", + "ownerId": "c3d4e5f6-a7b8-4c5d-0e1f-a2b3c4d5e6f7", + "name": "Missan", + "species": "Katt", + "breed": "Persisk", + "dateOfBirth": "2019-06-20", + "weightKg": 4.2, + "createdAt": "2025-01-01T10:00:00Z", + "updatedAt": "2025-01-01T10:00:00Z" + } +] +``` + +--- + +### 3.3 Skapa ett djur + +Registrera ett nytt djur för den inloggade ägaren. + +**`POST /pets`** + +```javascript +const skapaDjur = async () => { + const response = await fetch('http://localhost:8080/pets', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Dev-User': 'anna@test.se' + }, + body: JSON.stringify({ + name: 'Bella', + species: 'Hund', + breed: 'Golden Retriever', + dateOfBirth: '2022-03-10', + weightKg: 30.0 + }) + }); + const data = await response.json(); + return data; +}; +``` + +**Request body:** + +| Fält | Typ | Obligatoriskt | Beskrivning | +|-------------|----------|---------------|---------------------------| +| name | string | Ja | Djurets namn | +| species | string | Ja | Art (Hund, Katt, etc.) | +| breed | string | Nej | Ras | +| dateOfBirth | string | Ja | Födelsedatum (YYYY-MM-DD) | +| weightKg | number | Ja | Vikt i kg (positivt tal) | + +**Svar:** Samma format som i listan ovan (ett PetResponse-objekt). + +--- + +### 3.4 Hamta mina ärenden + +Visar alla veterinärarenden som tillhör den inloggade djurägaren. + +**`GET /api/medical-records/my-records`** + +```javascript +const getMinaÄrenden = async () => { + const response = await fetch('http://localhost:8080/api/medical-records/my-records', { + headers: { + 'X-Dev-User': 'anna@test.se' + } + }); + const data = await response.json(); + return data; +}; +``` + +**Svar (lista med ärenden i kortformat):** + +```json +[ + { + "id": "b8c9d0e1-f2a3-4b5c-5d6e-f7a8b9c0d1e2", + "title": "Fido haltar", + "status": "OPEN", + "petName": "Fido", + "ownerName": "Anna Svensson", + "assignedVetName": null, + "createdAt": "2025-01-01T10:00:00Z" + }, + { + "id": "c9d0e1f2-a3b4-4c5d-6e7f-a8b9c0d1e2f3", + "title": "Missan äter inte", + "status": "IN_PROGRESS", + "petName": "Missan", + "ownerName": "Anna Svensson", + "assignedVetName": "Erik Veterinär", + "createdAt": "2025-01-01T10:00:00Z" + } +] +``` + +Möjliga statusar: `OPEN`, `IN_PROGRESS`, `AWAITING_INFO`, `CLOSED` + +--- + +### 3.5 Skapa ett ärende + +Skapar ett nytt veterinärarende. Du behöver ett `petId` (fran 3.2) och ett `clinicId` (fran 3.1). + +**`POST /api/medical-records`** + +```javascript +const skapaÄrende = async () => { + const response = await fetch('http://localhost:8080/api/medical-records', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Dev-User': 'anna@test.se' + }, + body: JSON.stringify({ + title: 'Fido hostar', + description: 'Hunden har hostat i tre dagar och verkar trött.', + petId: 'f6a7b8c9-d0e1-4f5a-3b4c-d5e6f7a8b9c0', + clinicId: 'a1b2c3d4-e5f6-4a5b-8c9d-e0f1a2b3c4d5' + }) + }); + const data = await response.json(); + return data; +}; +``` + +**Request body:** + +| Fält | Typ | Obligatoriskt | Beskrivning | +|-------------|--------|---------------|---------------------------------| +| title | string | Ja | Kort titel (max 500 tecken) | +| description | string | Nej | Beskrivning (max 5000 tecken) | +| petId | string | Ja | UUID for djuret | +| clinicId | string | Ja | UUID for kliniken | + +**Svar (fullstandigt arende):** + +```json +{ + "id": "nytt-uuid-här", + "title": "Fido hostar", + "description": "Hunden har hostat i tre dagar och verkar trött.", + "status": "OPEN", + "petId": "f6a7b8c9-d0e1-4f5a-3b4c-d5e6f7a8b9c0", + "petName": "Fido", + "petSpecies": "Hund", + "ownerId": "c3d4e5f6-a7b8-4c5d-0e1f-a2b3c4d5e6f7", + "ownerName": "Anna Svensson", + "clinicId": "a1b2c3d4-e5f6-4a5b-8c9d-e0f1a2b3c4d5", + "clinicName": "Djurkliniken Centrum", + "assignedVetId": null, + "assignedVetName": null, + "createdById": "c3d4e5f6-a7b8-4c5d-0e1f-a2b3c4d5e6f7", + "createdByName": "Anna Svensson", + "createdAt": "2025-04-14T10:00:00Z", + "updatedAt": null, + "closedAt": null +} +``` + +--- + +### 3.6 Hamta ett specifikt arende + +Visar alla detaljer for ett arende. Anvands nar djurägaren klickar pa ett arende i listan. + +**`GET /api/medical-records/{id}`** + +```javascript +const getÄrende = async (ärendeId) => { + const response = await fetch(`http://localhost:8080/api/medical-records/${ärendeId}`, { + headers: { + 'X-Dev-User': 'anna@test.se' + } + }); + const data = await response.json(); + return data; +}; + +// Anvandning: +const ärende = await getÄrende('b8c9d0e1-f2a3-4b5c-5d6e-f7a8b9c0d1e2'); +``` + +**Svar:** Samma format som i 3.5 (MedicalRecordResponse). + +--- + +### 3.7 Skriva en kommentar + +Lagg till en kommentar pa ett arende. Tänk det som en chatt mellan djurägare och veterinär. + +**`POST /api/comments`** + +```javascript +const skapaKommentar = async (recordId, text) => { + const response = await fetch('http://localhost:8080/api/comments', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Dev-User': 'anna@test.se' + }, + body: JSON.stringify({ + recordId: recordId, + body: text + }) + }); + const data = await response.json(); + return data; +}; + +// Anvandning: +await skapaKommentar('b8c9d0e1-f2a3-4b5c-5d6e-f7a8b9c0d1e2', 'Fido verkar bättre idag!'); +``` + +**Request body:** + +| Fält | Typ | Obligatoriskt | Beskrivning | +|----------|--------|---------------|----------------------------------| +| recordId | string | Ja | UUID for ärendet | +| body | string | Ja | Kommentarstexten (max 5000 tecken) | + +**Svar:** + +```json +{ + "id": "nytt-uuid-här", + "recordId": "b8c9d0e1-f2a3-4b5c-5d6e-f7a8b9c0d1e2", + "authorId": "c3d4e5f6-a7b8-4c5d-0e1f-a2b3c4d5e6f7", + "authorName": "Anna Svensson", + "body": "Fido verkar bättre idag!", + "createdAt": "2025-04-14T10:00:00Z", + "updatedAt": null +} +``` + +--- + +### 3.8 Hamta kommentarer for ett arende + +Visar alla kommentarer pa ett arende, sorterade fran äldst till nyast. + +**`GET /api/comments/record/{recordId}`** + +```javascript +const getKommentarer = async (recordId) => { + const response = await fetch(`http://localhost:8080/api/comments/record/${recordId}`, { + headers: { + 'X-Dev-User': 'anna@test.se' + } + }); + const data = await response.json(); + return data; +}; + +// Anvandning: +const kommentarer = await getKommentarer('b8c9d0e1-f2a3-4b5c-5d6e-f7a8b9c0d1e2'); +``` + +**Svar (lista med kommentarer):** + +```json +[ + { + "id": "kommentar-uuid", + "recordId": "b8c9d0e1-f2a3-4b5c-5d6e-f7a8b9c0d1e2", + "authorId": "c3d4e5f6-a7b8-4c5d-0e1f-a2b3c4d5e6f7", + "authorName": "Anna Svensson", + "body": "Fido verkar bättre idag!", + "createdAt": "2025-04-14T10:00:00Z", + "updatedAt": null + } +] +``` + +--- + +### 3.9 Bifoga en fil + +Ladda upp en bild eller PDF till ett arende. Bilagor skickas som `multipart/form-data` — inte JSON. +Det ar samma format som ett vanligt HTML-formulär med ``. + +**`POST /api/attachments/record/{recordId}`** + +```javascript +const laddaUppFil = async (recordId, file, beskrivning) => { + // FormData anvands istället for JSON nar man skickar filer + const formData = new FormData(); + formData.append('file', file); // file = File-objekt fran + formData.append('description', beskrivning); // Valfri beskrivning + + const response = await fetch(`http://localhost:8080/api/attachments/record/${recordId}`, { + method: 'POST', + headers: { + 'X-Dev-User': 'anna@test.se' + // OBS: Satt INTE 'Content-Type' här — browsern lägger till det automatiskt + // med rätt boundary for multipart/form-data + }, + body: formData // Skicka FormData direkt, INTE JSON.stringify() + }); + const data = await response.json(); + return data; +}; + +// Anvandning i en React-komponent: +// laddaUppFil(recordId, e.target.files[0], 'Rontgenbild')} /> +``` + +**Tillåtna filtyper:** JPG, PNG, PDF +**Maxstorlek:** 10 MB + +**Svar (201 Created):** + +```json +{ + "id": "bilaga-uuid", + "recordId": "b8c9d0e1-f2a3-4b5c-5d6e-f7a8b9c0d1e2", + "fileName": "rontgen_fido.jpg", + "description": "Rontgenbild", + "fileType": "image/jpeg", + "fileSizeBytes": 245000, + "uploadedAt": "2025-04-14T10:00:00Z", + "uploadedBy": "Anna Svensson", + "downloadUrl": "https://minio.../presigned-url" +} +``` + +--- + +### 3.10 Hamta bilagor for ett arende + +Visar alla uppladdade filer pa ett arende. Varje bilaga har en `downloadUrl` som du kan anvanda som `src` i en `` eller som `href` i en ``. + +**`GET /api/attachments/record/{recordId}`** + +```javascript +const getBilagor = async (recordId) => { + const response = await fetch(`http://localhost:8080/api/attachments/record/${recordId}`, { + headers: { + 'X-Dev-User': 'anna@test.se' + } + }); + const data = await response.json(); + return data; +}; +``` + +**Svar (lista med bilagor):** + +```json +[ + { + "id": "bilaga-uuid", + "recordId": "b8c9d0e1-f2a3-4b5c-5d6e-f7a8b9c0d1e2", + "fileName": "rontgen_fido.jpg", + "description": "Rontgenbild", + "fileType": "image/jpeg", + "fileSizeBytes": 245000, + "uploadedAt": "2025-04-14T10:00:00Z", + "uploadedBy": "Anna Svensson", + "downloadUrl": "https://minio.../presigned-url" + } +] +``` + +**Tips:** `downloadUrl` ar en tidsbegränsad länk (giltig i 15 minuter). Om bilden inte laddas, hamta bilagorna igen for att fa en ny URL. + +--- + +## 4. Statuskoder att känna till + +Nar du anropar API:et far du alltid tillbaka en HTTP-statuskod. Kolla den i `response.status`. + +| Kod | Namn | Vad det betyder | +|-----|------------------|--------------------------------------------------------------| +| 200 | OK | Allt fungerade. Datan finns i response body. | +| 201 | Created | Resursen skapades. Datan finns i response body. | +| 204 | No Content | Lyckades, men inget data i svaret (t.ex. vid delete). | +| 400 | Bad Request | Något ar fel i din request. Kolla att alla fält ar ifyllda. | +| 403 | Forbidden | Du har inte rättighet. T.ex. en OWNER som försöker nå en VET-endpoint. | +| 404 | Not Found | Resursen finns inte. Kolla att UUID:t stämmer. | +| 500 | Server Error | Något gick fel i backend. Kolla IntelliJ-loggen. | + +**Felmeddelanden** kommer som JSON: + +```json +{ + "status": 400, + "message": "Validation failed", + "errors": { + "title": ["Titel far inte vara tom"] + } +} +``` + +--- + +## 5. Nar JWT ar implementerat + +Just nu anvander vi `X-Dev-User`-headern for att simulera inloggning. Nar JWT ar klart byter du ut den mot en riktig token. + +### Före (nu — utvecklingsläge) + +```javascript +headers: { + 'X-Dev-User': 'anna@test.se' +} +``` + +### Efter (med JWT) + +```javascript +// 1. Logga in och fa en token +const login = async (email, password) => { + const response = await fetch('http://localhost:8080/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }) + }); + const data = await response.json(); + return data.token; // Spara denna i state eller localStorage +}; + +// 2. Anvand token i alla andra anrop +headers: { + 'Authorization': `Bearer ${token}` +} +``` + +**Allt annat ar likadant** — samma URLer, samma request bodies, samma svar. Bara headern andras. + +--- + +## 6. Kända test-UUID:n + +Dessa UUID:n finns i testdatan som laddas vid uppstart. Anvand dem for att testa utan att behöva skapa data först. + +### Klinik + +| Namn | UUID | +|-----------------------|----------------------------------------| +| Djurkliniken Centrum | `a1b2c3d4-e5f6-4a5b-8c9d-e0f1a2b3c4d5` | + +### Anvandare + +| Namn | Roll | Email | UUID | +|-----------------|-------|----------------|----------------------------------------| +| Anna Svensson | OWNER | anna@test.se | `c3d4e5f6-a7b8-4c5d-0e1f-a2b3c4d5e6f7` | +| Erik Veterinär | VET | erik@klinik.se | `d4e5f6a7-b8c9-4d5e-1f2a-b3c4d5e6f7a8` | +| Sara Admin | ADMIN | sara@admin.se | `e5f6a7b8-c9d0-4e5f-2a3b-c4d5e6f7a8b9` | + +### Djur + +| Namn | Art | Ras | UUID | +|--------|------|-----------|----------------------------------------| +| Fido | Hund | Labrador | `f6a7b8c9-d0e1-4f5a-3b4c-d5e6f7a8b9c0` | +| Missan | Katt | Persisk | `a7b8c9d0-e1f2-4a5b-4c5d-e6f7a8b9c0d1` | + +### Arenden + +| Titel | Status | UUID | +|-----------------|-------------|----------------------------------------| +| Fido haltar | OPEN | `b8c9d0e1-f2a3-4b5c-5d6e-f7a8b9c0d1e2` | +| Missan äter inte | IN_PROGRESS | `c9d0e1f2-a3b4-4c5d-6e7f-a8b9c0d1e2f3` | + +--- + +## 7. Alla endpoints — komplett lista + +Nedan ar samtliga endpoints i API:et, grupperade per controller. +Kolumnen "Auth" visar vem som kan anropa endpointen. + +**Förkortningar:** +- **Alla** = ingen inloggning krävs (permitAll) +- **Inloggad** = alla inloggade anvandare +- **OWNER** = djurägare +- **VET** = veterinär +- **ADMIN** = administratör +- **Policy** = rollkontroll sker i service/policy-lagret, inte i controllern + +### Autentisering (`/api/auth`) + +*Dessa endpoints finns ännu inte — de skapas av Person B (se issue #1–3).* + +| Metod | URL | Auth | Beskrivning | Request body | +|-------|-----|------|-------------|--------------| +| POST | `/api/auth/register` | Alla | Registrera ny anvandare | `{ name, email, password, role }` | +| POST | `/api/auth/login` | Alla | Logga in, fa JWT-token | `{ email, password }` | + +### Kliniker (`/api/clinics`) + +| Metod | URL | Auth | Beskrivning | Request body | +|-------|-----|------|-------------|--------------| +| GET | `/api/clinics` | Alla | Hamta alla kliniker | — | +| GET | `/api/clinics/{id}` | Alla | Hamta en klinik | — | +| POST | `/api/clinics` | Inloggad | Skapa klinik | `{ name, address, phoneNumber }` | +| PUT | `/api/clinics/{id}` | Inloggad | Uppdatera klinik | `{ name, address, phoneNumber }` | +| DELETE | `/api/clinics/{id}` | Inloggad | Ta bort klinik | — | + +**Svar (ClinicResponse):** +```json +{ "id": "uuid", "name": "...", "address": "...", "phoneNumber": "..." } +``` + +### Djur (`/pets`) + +| Metod | URL | Auth | Beskrivning | Request body | +|-------|-----|------|-------------|--------------| +| POST | `/pets` | OWNER/ADMIN (Policy) | Skapa djur. Admin kan ange `?ownerId=uuid` | `{ name, species, breed, dateOfBirth, weightKg }` | +| GET | `/pets/{petId}` | OWNER/VET/ADMIN (Policy) | Hamta ett djur | — | +| GET | `/pets/owner/{ownerId}` | OWNER/ADMIN (Policy) | Hamta alla djur for en ägare | — | +| PUT | `/pets/{petId}` | OWNER/ADMIN (Policy) | Uppdatera djurinfo | `{ name, species, breed, dateOfBirth, weightKg }` | +| DELETE | `/pets/{petId}` | OWNER/ADMIN (Policy) | Ta bort djur | — | + +**Svar (PetResponse):** +```json +{ + "id": "uuid", "ownerId": "uuid", "name": "Fido", "species": "Hund", + "breed": "Labrador", "dateOfBirth": "2020-01-15", "weightKg": 25.5, + "createdAt": "2025-01-01T10:00:00Z", "updatedAt": "2025-01-01T10:00:00Z" +} +``` + +### Arenden (`/api/medical-records`) + +| Metod | URL | Auth | Beskrivning | Request body | +|-------|-----|------|-------------|--------------| +| POST | `/api/medical-records` | Inloggad (Policy) | Skapa arende | `{ title, description, petId, clinicId }` | +| GET | `/api/medical-records/{id}` | Inloggad (Policy) | Hamta ett arende (fullstandig) | — | +| GET | `/api/medical-records/my-records` | OWNER | Hamta mina ärenden (kortformat) | — | +| GET | `/api/medical-records/owner/{ownerId}` | OWNER (egna)/VET/ADMIN | Hamta ärenden per ägare | — | +| GET | `/api/medical-records/pet/{petId}` | Inloggad (Policy) | Hamta ärenden per djur | — | +| GET | `/api/medical-records/clinic/{clinicId}` | VET/ADMIN (Policy) | Hamta ärenden per klinik | — | +| GET | `/api/medical-records/clinic/{clinicId}/status/{status}` | VET/ADMIN (Policy) | Hamta ärenden per klinik + status | — | +| PUT | `/api/medical-records/{id}` | VET/ADMIN (Policy) | Uppdatera titel/beskrivning | `{ title, description }` | +| PUT | `/api/medical-records/{id}/assign-vet` | VET/ADMIN (Policy) | Tilldela veterinär | `{ vetId }` | +| PUT | `/api/medical-records/{id}/status` | VET/ADMIN (Policy) | Ändra status | `{ status }` | +| PUT | `/api/medical-records/{id}/close` | VET/ADMIN (Policy) | Stäng ärende | — | + +**Möjliga statusar:** `OPEN`, `IN_PROGRESS`, `AWAITING_INFO`, `CLOSED` + +**Svar — fullständigt (MedicalRecordResponse):** +```json +{ + "id": "uuid", "title": "...", "description": "...", "status": "OPEN", + "petId": "uuid", "petName": "Fido", "petSpecies": "Hund", + "ownerId": "uuid", "ownerName": "Anna Svensson", + "clinicId": "uuid", "clinicName": "Djurkliniken Centrum", + "assignedVetId": null, "assignedVetName": null, + "createdById": "uuid", "createdByName": "Anna Svensson", + "createdAt": "2025-01-01T10:00:00Z", "updatedAt": null, "closedAt": null +} +``` + +**Svar — kortformat (MedicalRecordSummaryResponse):** +```json +{ + "id": "uuid", "title": "...", "status": "OPEN", + "petName": "Fido", "ownerName": "Anna Svensson", + "assignedVetName": null, "createdAt": "2025-01-01T10:00:00Z" +} +``` + +### Kommentarer (`/api/comments`) + +| Metod | URL | Auth | Beskrivning | Request body | +|-------|-----|------|-------------|--------------| +| POST | `/api/comments` | Inloggad (Policy) | Skapa kommentar | `{ recordId, body }` | +| GET | `/api/comments/record/{recordId}` | Inloggad (Policy) | Hamta kommentarer for ett arende | — | +| GET | `/api/comments/record/{recordId}/count` | Inloggad (Policy) | Antal kommentarer pa ett arende | — | +| PUT | `/api/comments/{id}` | Inloggad (Policy) | Uppdatera kommentar | `{ body }` | +| DELETE | `/api/comments/{id}` | Inloggad (Policy) | Ta bort kommentar | — | + +**Svar (CommentResponse):** +```json +{ + "id": "uuid", "recordId": "uuid", "authorId": "uuid", + "authorName": "Anna Svensson", "body": "Fido verkar bättre!", + "createdAt": "2025-04-14T10:00:00Z", "updatedAt": null +} +``` + +### Bilagor (`/api/attachments`) + +| Metod | URL | Auth | Beskrivning | Request body | +|-------|-----|------|-------------|--------------| +| POST | `/api/attachments/record/{recordId}` | Inloggad (Policy) | Ladda upp fil (multipart/form-data) | `file` + valfri `description` | +| GET | `/api/attachments/record/{recordId}` | Inloggad (Policy) | Hamta bilagor for ett arende | — | +| GET | `/api/attachments/{id}/download` | Inloggad (Policy) | Hamta en bilaga (presigned URL) | — | +| DELETE | `/api/attachments/{id}` | VET/ADMIN (Policy) | Ta bort bilaga | — | + +**Tillåtna filtyper:** JPG, PNG, PDF. **Max:** 10 MB. + +**Svar (AttachmentResponse):** +```json +{ + "id": "uuid", "recordId": "uuid", "fileName": "bild.jpg", + "description": "Rontgenbild", "fileType": "image/jpeg", + "fileSizeBytes": 245000, "uploadedAt": "2025-04-14T10:00:00Z", + "uploadedBy": "Anna Svensson", "downloadUrl": "https://presigned-url..." +} +``` + +### Aktivitetslogg (`/api/activity-logs`) + +| Metod | URL | Auth | Beskrivning | Request body | +|-------|-----|------|-------------|--------------| +| GET | `/api/activity-logs/record/{recordId}` | Inloggad (Policy) | Hamta händelselogg for ett arende | — | + +**Svar (lista med ActivityLogResponse):** +```json +[ + { + "id": "uuid", "action": "CASE_CREATED", "description": "Ärende skapat", + "performedById": "uuid", "performedByName": "Anna Svensson", + "recordId": "uuid", "createdAt": "2025-01-01T10:00:00Z" + } +] +``` + +**Möjliga actions:** `CASE_CREATED`, `UPDATED`, `STATUS_CHANGED`, `ASSIGNED`, `COMMENT_ADDED` + +### Anvandare (`/api/users`) + +| Metod | URL | Auth | Beskrivning | Request body | +|-------|-----|------|-------------|--------------| +| GET | `/api/users` | Inloggad | Hamta alla anvandare | — | +| GET | `/api/users/{id}` | Inloggad | Hamta en anvandare | — | +| POST | `/api/users` | Inloggad | Skapa anvandare | `{ name, email, password, role, clinicId? }` | +| PUT | `/api/users/{id}` | Inloggad | Uppdatera anvandare | `{ name?, email?, clinicId? }` | +| DELETE | `/api/users/{id}` | Inloggad | Ta bort anvandare | — | + +**Svar (UserResponse):** +```json +{ + "id": "uuid", "name": "Anna Svensson", "email": "anna@test.se", + "role": "OWNER", "clinicId": null, + "createdAt": "2025-01-01T10:00:00Z", "updatedAt": "2025-01-01T10:00:00Z" +} +``` + +### Veterinärer (`/api/vets`) + +| Metod | URL | Auth | Beskrivning | Request body | +|-------|-----|------|-------------|--------------| +| GET | `/api/vets` | Inloggad | Hamta alla veterinärer | — | +| GET | `/api/vets/{id}` | Inloggad | Hamta en veterinär | — | +| POST | `/api/vets` | ADMIN | Skapa veterinärprofil | `{ userId, licenseId, specialization?, bookingInfo? }` | + +**Svar (VetResponse):** +```json +{ + "userId": "uuid", "name": "Erik Veterinär", "email": "erik@klinik.se", + "licenseId": "VET-001", "specialization": "Ortopedi", + "bookingInfo": "Mån-Fre 08-17", "clinicName": "Djurkliniken Centrum", + "isActive": true +} From 965dcf1c6dcf5c011cab564693e01f8fd1a5de7d Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Tue, 14 Apr 2026 10:15:28 +0200 Subject: [PATCH 41/85] test: clean up comments and update application-test.properties - Removed redundant and commented-out lines in `application-test.properties`. - Ensured properties have clear and concise descriptions for better readability. --- src/test/resources/application-test.properties | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties index 4f351edc..0fcfdc88 100644 --- a/src/test/resources/application-test.properties +++ b/src/test/resources/application-test.properties @@ -1,9 +1,8 @@ # ============================================ -# TEST-profil ? k�rs av @ActiveProfiles("test") -# Anv�nds av integrationstester med @SpringBootTest + # ============================================ -# Separat testdatabas ? aldrig dev-databasen + spring.datasource.url=jdbc:h2:mem:testdb;MODE=PostgreSQL spring.datasource.driver-class-name=org.h2.Driver spring.datasource.username=sa @@ -13,14 +12,14 @@ spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.datasource.hikari.driver-class-name=org.h2.Driver -# Skapa schema fr�n scratch och rensa efter tester + spring.jpa.hibernate.ddl-auto=create-drop # Hibernate creates schema automatically (schema.sql is NOT used in tests) spring.sql.init.mode=never spring.sql.init.data-locations= -# St�ng av Docker Compose i tester + # CI hanterar databasen som service container spring.docker.compose.lifecycle-management=none @@ -31,7 +30,7 @@ aws.s3.secret-key=minioadmin aws.s3.bucket-name=vet1177-test-attachments aws.s3.region=eu-north-1 -# JWT ? dummyv�rden f�r tester + jwt.secret-key=test-secret-key-minst-32-tecken-lang-for-tester jwt.expiration-ms=86400000 From 4975a1dcdf28f625f7422bcb2c18cbabd7ebc311 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Tue, 14 Apr 2026 11:22:41 +0200 Subject: [PATCH 42/85] fix: add missing security mocks to PetControllerTest and UserControllerTest --- .../vet1177/controller/PetControllerTest.java | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/test/java/org/example/vet1177/controller/PetControllerTest.java b/src/test/java/org/example/vet1177/controller/PetControllerTest.java index 2564605e..32ae72b6 100644 --- a/src/test/java/org/example/vet1177/controller/PetControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/PetControllerTest.java @@ -1,6 +1,9 @@ package org.example.vet1177.controller; //Används i commentControllerTest också, verkar funka där? +import org.example.vet1177.security.CustomUserDetailsService; +import org.example.vet1177.security.JwtService; +import org.springframework.test.context.ActiveProfiles; import tools.jackson.databind.ObjectMapper; import org.example.vet1177.exception.ResourceNotFoundException; import org.example.vet1177.dto.request.pet.PetRequest; @@ -35,6 +38,7 @@ @WebMvcTest(PetController.class) @Import(SecurityConfig.class) +@ActiveProfiles("test") public class PetControllerTest { @Autowired @@ -46,25 +50,28 @@ public class PetControllerTest { @MockitoBean private UserService userService; + @MockitoBean + private CustomUserDetailsService customUserDetailsService; + + @MockitoBean + private JwtService jwtService; + private User owner; private User vet; private Pet pet; private UUID ownerId; private UUID petId; + + @BeforeEach - void setUp() { + void setUp() throws Exception { ownerId = UUID.randomUUID(); petId = UUID.randomUUID(); owner = new User("Anna Ägare", "anna@mail.se", "hash", Role.OWNER); vet = new User("Dr. Erik Vet", "erik@vet.se", "hash", Role.VET); pet = new Pet(owner, "Molly", "Hund", "Labrador", LocalDate.of(2020, 1, 1), new BigDecimal("12.50")); } - private RequestPostProcessor authenticatedAs(User user) { - return authentication(new UsernamePasswordAuthenticationToken( - user, null, user.getAuthorities() - )); - } private PetRequest validPetRequest() { PetRequest request = new PetRequest(); @@ -76,6 +83,12 @@ private PetRequest validPetRequest() { return request; } + private RequestPostProcessor authenticatedAs(User user) { + return authentication(new UsernamePasswordAuthenticationToken( + user, null, user.getAuthorities() + )); + } + //POST /pets @Test void createPet_shouldReturn200WithPetResponse() throws Exception { From 140301faa630eb9aae0cdf5a15e1594994b1d51c Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Mon, 13 Apr 2026 12:03:08 +0200 Subject: [PATCH 43/85] Test: GET /api/users and GET /api/users/{id} --- .../controller/UserControllerTest.java | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/test/java/org/example/vet1177/controller/UserControllerTest.java diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java new file mode 100644 index 00000000..36cddac0 --- /dev/null +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -0,0 +1,114 @@ +package org.example.vet1177.controller; + +import tools.jackson.databind.ObjectMapper; +import org.example.vet1177.dto.request.user.UserRequest; + +import org.example.vet1177.dto.response.user.UserResponse; +import org.example.vet1177.entities.Role; +import org.example.vet1177.entities.User; + +import org.example.vet1177.exception.ResourceNotFoundException; +import org.example.vet1177.security.SecurityConfig; +import org.example.vet1177.services.UserService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.context.annotation.Import; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.RequestPostProcessor; + +import java.util.List; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +@WebMvcTest(UserController.class) +@Import(SecurityConfig.class) +class UserControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockitoBean + private UserService userService; + + private User currentUser; + private UserResponse userResponse; + private UUID userId; + + @BeforeEach + void setUp() { + userId = UUID.randomUUID(); + currentUser = new User("Anna Karlsson", "anna@example.se", "hash", Role.ADMIN); + userResponse = new UserResponse(userId, "Anna Karlsson", "anna@example.se", Role.OWNER, null, null, null); + } + + private RequestPostProcessor authenticatedAs(User user) { + return authentication(new UsernamePasswordAuthenticationToken( + user, null, user.getAuthorities() + )); + } + + private UserRequest validUserRequest() { + UserRequest request = new UserRequest(); + request.setName("Anna Karlsson"); + request.setEmail("anna@example.se"); + request.setPassword("lösenord123"); + request.setRole(Role.OWNER); + return request; + } + + + // GET /api/users + + @Test + void getAllUsers_shouldReturn200WithList() throws Exception { + when(userService.getAllUsers()).thenReturn(List.of(userResponse)); + + mockMvc.perform(get("/api/users") + .with(authenticatedAs(currentUser))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].name").value("Anna Karlsson")); + } + + @Test + void getAllUsers_whenEmpty_shouldReturnEmptyList() throws Exception { + when(userService.getAllUsers()).thenReturn(List.of()); + + mockMvc.perform(get("/api/users") + .with(authenticatedAs(currentUser))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isEmpty()); + } + + // GET /api/users/{id} + + @Test + void getUserById_shouldReturn200WithUserResponse() throws Exception { + when(userService.getById(userId)).thenReturn(userResponse); + + mockMvc.perform(get("/api/users/{id}", userId) + .with(authenticatedAs(currentUser))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("Anna Karlsson")); + } + + @Test + void getUserById_whenNotFound_shouldReturn404() throws Exception { + when(userService.getById(any())) + .thenThrow(new ResourceNotFoundException("User", userId)); + + mockMvc.perform(get("/api/users/{id}", userId) + .with(authenticatedAs(currentUser))) + .andExpect(status().isNotFound()); + }} From 8d618331d387393a11ff3d0977d0765117bfc8cd Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Mon, 13 Apr 2026 15:58:08 +0200 Subject: [PATCH 44/85] Test:POST /api/users, PUT /api/users/{id} --- .../controller/UserControllerTest.java | 97 ++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index 36cddac0..e6626da8 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -1,5 +1,8 @@ package org.example.vet1177.controller; +import org.example.vet1177.dto.request.user.UserUpdateRequest; +import org.example.vet1177.exception.BusinessRuleException; +import org.springframework.http.MediaType; import tools.jackson.databind.ObjectMapper; import org.example.vet1177.dto.request.user.UserRequest; @@ -111,4 +114,96 @@ void getUserById_whenNotFound_shouldReturn404() throws Exception { mockMvc.perform(get("/api/users/{id}", userId) .with(authenticatedAs(currentUser))) .andExpect(status().isNotFound()); - }} + } + + + // POST /api/users + + + @Test + void createUser_shouldReturn201WithUserResponse() throws Exception { + when(userService.createUser(any())).thenReturn(userResponse); + + mockMvc.perform(post("/api/users") + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(validUserRequest()))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.name").value("Anna Karlsson")); + } + + @Test + void createUser_whenInvalidRequest_shouldReturn400() throws Exception { + UserRequest request = validUserRequest(); + request.setEmail("inte-en-email"); + + mockMvc.perform(post("/api/users") + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + void createUser_whenEmailAlreadyTaken_shouldReturn400() throws Exception { + when(userService.createUser(any())) + .thenThrow(new BusinessRuleException("Email används redan")); + + mockMvc.perform(post("/api/users") + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(validUserRequest()))) + .andExpect(status().isBadRequest()); + } + + + // PUT /api/users/{id} + + + @Test + void updateUser_shouldReturn200WithUpdatedUserResponse() throws Exception { + UserResponse updated = new UserResponse(userId, "Uppdaterad Namn", "anna@example.se", Role.OWNER, null, null, null); + when(userService.updateUser(eq(userId), any())).thenReturn(updated); + + UserUpdateRequest request = new UserUpdateRequest(); + request.setName("Uppdaterad Namn"); + + mockMvc.perform(put("/api/users/{id}", userId) + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("Uppdaterad Namn")); + } + + @Test + void updateUser_whenNotFound_shouldReturn404() throws Exception { + when(userService.updateUser(any(), any())) + .thenThrow(new ResourceNotFoundException("User", userId)); + + UserUpdateRequest request = new UserUpdateRequest(); + request.setName("Uppdaterad Namn"); + + mockMvc.perform(put("/api/users/{id}", userId) + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()); + } + + @Test + void updateUser_whenBusinessRuleViolation_shouldReturn400() throws Exception { + when(userService.updateUser(any(), any())) + .thenThrow(new BusinessRuleException("Email används redan")); + + UserUpdateRequest request = new UserUpdateRequest(); + request.setEmail("tagen@example.se"); + + mockMvc.perform(put("/api/users/{id}", userId) + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + +} From dad0c7652deb6b90f525a4f44d7f55cb4e8855eb Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Mon, 13 Apr 2026 17:17:23 +0200 Subject: [PATCH 45/85] Test: DELETE /api/users/{id} --- .../controller/UserControllerTest.java | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index e6626da8..a3090949 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -116,10 +116,7 @@ void getUserById_whenNotFound_shouldReturn404() throws Exception { .andExpect(status().isNotFound()); } - // POST /api/users - - @Test void createUser_shouldReturn201WithUserResponse() throws Exception { when(userService.createUser(any())).thenReturn(userResponse); @@ -206,4 +203,35 @@ void updateUser_whenBusinessRuleViolation_shouldReturn400() throws Exception { .andExpect(status().isBadRequest()); } + // DELETE /api/users/{id} + + @Test + void deleteUser_shouldReturn204() throws Exception { + doNothing().when(userService).deleteUser(userId); + + mockMvc.perform(delete("/api/users/{id}", userId) + .with(authenticatedAs(currentUser))) + .andExpect(status().isNoContent()); + } + + @Test + void deleteUser_whenNotFound_shouldReturn404() throws Exception { + doThrow(new ResourceNotFoundException("User", userId)) + .when(userService).deleteUser(any()); + + mockMvc.perform(delete("/api/users/{id}", userId) + .with(authenticatedAs(currentUser))) + .andExpect(status().isNotFound()); + } + + @Test + void deleteUser_whenHasLinkedResources_shouldReturn400() throws Exception { + doThrow(new BusinessRuleException("Användaren har kopplade djur och kan inte raderas")) + .when(userService).deleteUser(any()); + + mockMvc.perform(delete("/api/users/{id}", userId) + .with(authenticatedAs(currentUser))) + .andExpect(status().isBadRequest()); + } } + From d5aafe0254e5bed0b4ff9f0bd5baeae3a722180d Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Tue, 14 Apr 2026 10:48:13 +0200 Subject: [PATCH 46/85] rabbit fixes --- .../vet1177/controller/UserControllerTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index a3090949..490cdcdc 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -139,6 +139,20 @@ void createUser_whenInvalidRequest_shouldReturn400() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isBadRequest()); + verify(userService, never()).createUser(any()); + } + + @Test + void createUser_whenNameMissing_shouldReturn400() throws Exception { + UserRequest request = validUserRequest(); + request.setName(""); + + mockMvc.perform(post("/api/users") + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + verify(userService, never()).createUser(any()); } @Test From 2f200c6057ae640c04b95f9e56d17e9945fad0a3 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Tue, 14 Apr 2026 10:54:37 +0200 Subject: [PATCH 47/85] rabbit fixes --- .../vet1177/controller/UserControllerTest.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index 490cdcdc..c62ef3f8 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -142,19 +142,6 @@ void createUser_whenInvalidRequest_shouldReturn400() throws Exception { verify(userService, never()).createUser(any()); } - @Test - void createUser_whenNameMissing_shouldReturn400() throws Exception { - UserRequest request = validUserRequest(); - request.setName(""); - - mockMvc.perform(post("/api/users") - .with(authenticatedAs(currentUser)) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isBadRequest()); - verify(userService, never()).createUser(any()); - } - @Test void createUser_whenEmailAlreadyTaken_shouldReturn400() throws Exception { when(userService.createUser(any())) From c72684e55f0b0f3580f568df6ee93cd0ed6f8953 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Tue, 14 Apr 2026 11:02:40 +0200 Subject: [PATCH 48/85] make CI work --- .../org/example/vet1177/controller/UserControllerTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index c62ef3f8..8d07259a 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -3,6 +3,7 @@ import org.example.vet1177.dto.request.user.UserUpdateRequest; import org.example.vet1177.exception.BusinessRuleException; import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; import tools.jackson.databind.ObjectMapper; import org.example.vet1177.dto.request.user.UserRequest; @@ -32,8 +33,10 @@ import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + @WebMvcTest(UserController.class) @Import(SecurityConfig.class) +@ActiveProfiles("test") class UserControllerTest { @Autowired From 3d8b5cc44b4fde738637c72b2b300444407742f4 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Tue, 14 Apr 2026 11:39:49 +0200 Subject: [PATCH 49/85] fix: add missing security mocks to UserControllerTest --- .../org/example/vet1177/controller/UserControllerTest.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index 8d07259a..b12c3e4e 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -52,6 +52,12 @@ class UserControllerTest { private UserResponse userResponse; private UUID userId; + @MockitoBean + private org.example.vet1177.security.JwtService jwtService; + @MockitoBean + private org.example.vet1177.security.CustomUserDetailsService customUserDetailsService; + + @BeforeEach void setUp() { userId = UUID.randomUUID(); From 68bf569cf2b4a4c9d79616fc1795da4775d9a13e Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Tue, 14 Apr 2026 11:42:22 +0200 Subject: [PATCH 50/85] fix: add missing security mocks to UserControllerTest --- .../vet1177/controller/PetControllerTest.java | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/test/java/org/example/vet1177/controller/PetControllerTest.java b/src/test/java/org/example/vet1177/controller/PetControllerTest.java index 2564605e..32ae72b6 100644 --- a/src/test/java/org/example/vet1177/controller/PetControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/PetControllerTest.java @@ -1,6 +1,9 @@ package org.example.vet1177.controller; //Används i commentControllerTest också, verkar funka där? +import org.example.vet1177.security.CustomUserDetailsService; +import org.example.vet1177.security.JwtService; +import org.springframework.test.context.ActiveProfiles; import tools.jackson.databind.ObjectMapper; import org.example.vet1177.exception.ResourceNotFoundException; import org.example.vet1177.dto.request.pet.PetRequest; @@ -35,6 +38,7 @@ @WebMvcTest(PetController.class) @Import(SecurityConfig.class) +@ActiveProfiles("test") public class PetControllerTest { @Autowired @@ -46,25 +50,28 @@ public class PetControllerTest { @MockitoBean private UserService userService; + @MockitoBean + private CustomUserDetailsService customUserDetailsService; + + @MockitoBean + private JwtService jwtService; + private User owner; private User vet; private Pet pet; private UUID ownerId; private UUID petId; + + @BeforeEach - void setUp() { + void setUp() throws Exception { ownerId = UUID.randomUUID(); petId = UUID.randomUUID(); owner = new User("Anna Ägare", "anna@mail.se", "hash", Role.OWNER); vet = new User("Dr. Erik Vet", "erik@vet.se", "hash", Role.VET); pet = new Pet(owner, "Molly", "Hund", "Labrador", LocalDate.of(2020, 1, 1), new BigDecimal("12.50")); } - private RequestPostProcessor authenticatedAs(User user) { - return authentication(new UsernamePasswordAuthenticationToken( - user, null, user.getAuthorities() - )); - } private PetRequest validPetRequest() { PetRequest request = new PetRequest(); @@ -76,6 +83,12 @@ private PetRequest validPetRequest() { return request; } + private RequestPostProcessor authenticatedAs(User user) { + return authentication(new UsernamePasswordAuthenticationToken( + user, null, user.getAuthorities() + )); + } + //POST /pets @Test void createPet_shouldReturn200WithPetResponse() throws Exception { From e6c8235711212459b1672cb6d3487512b5042b6b Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Mon, 13 Apr 2026 12:03:08 +0200 Subject: [PATCH 51/85] Test: GET /api/users and GET /api/users/{id} # Conflicts: # src/test/java/org/example/vet1177/controller/UserControllerTest.java --- .../controller/UserControllerTest.java | 134 +----------------- 1 file changed, 1 insertion(+), 133 deletions(-) diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index b12c3e4e..25e21a81 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -1,9 +1,5 @@ package org.example.vet1177.controller; -import org.example.vet1177.dto.request.user.UserUpdateRequest; -import org.example.vet1177.exception.BusinessRuleException; -import org.springframework.http.MediaType; -import org.springframework.test.context.ActiveProfiles; import tools.jackson.databind.ObjectMapper; import org.example.vet1177.dto.request.user.UserRequest; @@ -33,10 +29,8 @@ import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - @WebMvcTest(UserController.class) @Import(SecurityConfig.class) -@ActiveProfiles("test") class UserControllerTest { @Autowired @@ -52,12 +46,6 @@ class UserControllerTest { private UserResponse userResponse; private UUID userId; - @MockitoBean - private org.example.vet1177.security.JwtService jwtService; - @MockitoBean - private org.example.vet1177.security.CustomUserDetailsService customUserDetailsService; - - @BeforeEach void setUp() { userId = UUID.randomUUID(); @@ -124,124 +112,4 @@ void getUserById_whenNotFound_shouldReturn404() throws Exception { .with(authenticatedAs(currentUser))) .andExpect(status().isNotFound()); } - - // POST /api/users - @Test - void createUser_shouldReturn201WithUserResponse() throws Exception { - when(userService.createUser(any())).thenReturn(userResponse); - - mockMvc.perform(post("/api/users") - .with(authenticatedAs(currentUser)) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(validUserRequest()))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$.name").value("Anna Karlsson")); - } - - @Test - void createUser_whenInvalidRequest_shouldReturn400() throws Exception { - UserRequest request = validUserRequest(); - request.setEmail("inte-en-email"); - - mockMvc.perform(post("/api/users") - .with(authenticatedAs(currentUser)) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isBadRequest()); - verify(userService, never()).createUser(any()); - } - - @Test - void createUser_whenEmailAlreadyTaken_shouldReturn400() throws Exception { - when(userService.createUser(any())) - .thenThrow(new BusinessRuleException("Email används redan")); - - mockMvc.perform(post("/api/users") - .with(authenticatedAs(currentUser)) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(validUserRequest()))) - .andExpect(status().isBadRequest()); - } - - - // PUT /api/users/{id} - - - @Test - void updateUser_shouldReturn200WithUpdatedUserResponse() throws Exception { - UserResponse updated = new UserResponse(userId, "Uppdaterad Namn", "anna@example.se", Role.OWNER, null, null, null); - when(userService.updateUser(eq(userId), any())).thenReturn(updated); - - UserUpdateRequest request = new UserUpdateRequest(); - request.setName("Uppdaterad Namn"); - - mockMvc.perform(put("/api/users/{id}", userId) - .with(authenticatedAs(currentUser)) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.name").value("Uppdaterad Namn")); - } - - @Test - void updateUser_whenNotFound_shouldReturn404() throws Exception { - when(userService.updateUser(any(), any())) - .thenThrow(new ResourceNotFoundException("User", userId)); - - UserUpdateRequest request = new UserUpdateRequest(); - request.setName("Uppdaterad Namn"); - - mockMvc.perform(put("/api/users/{id}", userId) - .with(authenticatedAs(currentUser)) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isNotFound()); - } - - @Test - void updateUser_whenBusinessRuleViolation_shouldReturn400() throws Exception { - when(userService.updateUser(any(), any())) - .thenThrow(new BusinessRuleException("Email används redan")); - - UserUpdateRequest request = new UserUpdateRequest(); - request.setEmail("tagen@example.se"); - - mockMvc.perform(put("/api/users/{id}", userId) - .with(authenticatedAs(currentUser)) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isBadRequest()); - } - - // DELETE /api/users/{id} - - @Test - void deleteUser_shouldReturn204() throws Exception { - doNothing().when(userService).deleteUser(userId); - - mockMvc.perform(delete("/api/users/{id}", userId) - .with(authenticatedAs(currentUser))) - .andExpect(status().isNoContent()); - } - - @Test - void deleteUser_whenNotFound_shouldReturn404() throws Exception { - doThrow(new ResourceNotFoundException("User", userId)) - .when(userService).deleteUser(any()); - - mockMvc.perform(delete("/api/users/{id}", userId) - .with(authenticatedAs(currentUser))) - .andExpect(status().isNotFound()); - } - - @Test - void deleteUser_whenHasLinkedResources_shouldReturn400() throws Exception { - doThrow(new BusinessRuleException("Användaren har kopplade djur och kan inte raderas")) - .when(userService).deleteUser(any()); - - mockMvc.perform(delete("/api/users/{id}", userId) - .with(authenticatedAs(currentUser))) - .andExpect(status().isBadRequest()); - } -} - +} \ No newline at end of file From 523d8bc5df00a2e9b8a75473cf8b81fd3931d07d Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Mon, 13 Apr 2026 15:58:08 +0200 Subject: [PATCH 52/85] Test:POST /api/users, PUT /api/users/{id} --- .../controller/UserControllerTest.java | 96 ++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index 25e21a81..e6626da8 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -1,5 +1,8 @@ package org.example.vet1177.controller; +import org.example.vet1177.dto.request.user.UserUpdateRequest; +import org.example.vet1177.exception.BusinessRuleException; +import org.springframework.http.MediaType; import tools.jackson.databind.ObjectMapper; import org.example.vet1177.dto.request.user.UserRequest; @@ -112,4 +115,95 @@ void getUserById_whenNotFound_shouldReturn404() throws Exception { .with(authenticatedAs(currentUser))) .andExpect(status().isNotFound()); } -} \ No newline at end of file + + + // POST /api/users + + + @Test + void createUser_shouldReturn201WithUserResponse() throws Exception { + when(userService.createUser(any())).thenReturn(userResponse); + + mockMvc.perform(post("/api/users") + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(validUserRequest()))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.name").value("Anna Karlsson")); + } + + @Test + void createUser_whenInvalidRequest_shouldReturn400() throws Exception { + UserRequest request = validUserRequest(); + request.setEmail("inte-en-email"); + + mockMvc.perform(post("/api/users") + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + void createUser_whenEmailAlreadyTaken_shouldReturn400() throws Exception { + when(userService.createUser(any())) + .thenThrow(new BusinessRuleException("Email används redan")); + + mockMvc.perform(post("/api/users") + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(validUserRequest()))) + .andExpect(status().isBadRequest()); + } + + + // PUT /api/users/{id} + + + @Test + void updateUser_shouldReturn200WithUpdatedUserResponse() throws Exception { + UserResponse updated = new UserResponse(userId, "Uppdaterad Namn", "anna@example.se", Role.OWNER, null, null, null); + when(userService.updateUser(eq(userId), any())).thenReturn(updated); + + UserUpdateRequest request = new UserUpdateRequest(); + request.setName("Uppdaterad Namn"); + + mockMvc.perform(put("/api/users/{id}", userId) + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("Uppdaterad Namn")); + } + + @Test + void updateUser_whenNotFound_shouldReturn404() throws Exception { + when(userService.updateUser(any(), any())) + .thenThrow(new ResourceNotFoundException("User", userId)); + + UserUpdateRequest request = new UserUpdateRequest(); + request.setName("Uppdaterad Namn"); + + mockMvc.perform(put("/api/users/{id}", userId) + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()); + } + + @Test + void updateUser_whenBusinessRuleViolation_shouldReturn400() throws Exception { + when(userService.updateUser(any(), any())) + .thenThrow(new BusinessRuleException("Email används redan")); + + UserUpdateRequest request = new UserUpdateRequest(); + request.setEmail("tagen@example.se"); + + mockMvc.perform(put("/api/users/{id}", userId) + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + +} From ff78a1208c4165ecf83c21e41cd76c979791d20d Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Mon, 13 Apr 2026 17:17:23 +0200 Subject: [PATCH 53/85] Test: DELETE /api/users/{id} --- .../controller/UserControllerTest.java | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index e6626da8..a3090949 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -116,10 +116,7 @@ void getUserById_whenNotFound_shouldReturn404() throws Exception { .andExpect(status().isNotFound()); } - // POST /api/users - - @Test void createUser_shouldReturn201WithUserResponse() throws Exception { when(userService.createUser(any())).thenReturn(userResponse); @@ -206,4 +203,35 @@ void updateUser_whenBusinessRuleViolation_shouldReturn400() throws Exception { .andExpect(status().isBadRequest()); } + // DELETE /api/users/{id} + + @Test + void deleteUser_shouldReturn204() throws Exception { + doNothing().when(userService).deleteUser(userId); + + mockMvc.perform(delete("/api/users/{id}", userId) + .with(authenticatedAs(currentUser))) + .andExpect(status().isNoContent()); + } + + @Test + void deleteUser_whenNotFound_shouldReturn404() throws Exception { + doThrow(new ResourceNotFoundException("User", userId)) + .when(userService).deleteUser(any()); + + mockMvc.perform(delete("/api/users/{id}", userId) + .with(authenticatedAs(currentUser))) + .andExpect(status().isNotFound()); + } + + @Test + void deleteUser_whenHasLinkedResources_shouldReturn400() throws Exception { + doThrow(new BusinessRuleException("Användaren har kopplade djur och kan inte raderas")) + .when(userService).deleteUser(any()); + + mockMvc.perform(delete("/api/users/{id}", userId) + .with(authenticatedAs(currentUser))) + .andExpect(status().isBadRequest()); + } } + From a34d43c91d46eea799514de42881e0e39f802976 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Tue, 14 Apr 2026 10:48:13 +0200 Subject: [PATCH 54/85] rabbit fixes --- .../vet1177/controller/UserControllerTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index a3090949..490cdcdc 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -139,6 +139,20 @@ void createUser_whenInvalidRequest_shouldReturn400() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isBadRequest()); + verify(userService, never()).createUser(any()); + } + + @Test + void createUser_whenNameMissing_shouldReturn400() throws Exception { + UserRequest request = validUserRequest(); + request.setName(""); + + mockMvc.perform(post("/api/users") + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + verify(userService, never()).createUser(any()); } @Test From 9d222d1eaf6d83a2e6ddfe0edcbf2619b3a0d783 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Tue, 14 Apr 2026 10:54:37 +0200 Subject: [PATCH 55/85] rabbit fixes --- .../vet1177/controller/UserControllerTest.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index 490cdcdc..c62ef3f8 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -142,19 +142,6 @@ void createUser_whenInvalidRequest_shouldReturn400() throws Exception { verify(userService, never()).createUser(any()); } - @Test - void createUser_whenNameMissing_shouldReturn400() throws Exception { - UserRequest request = validUserRequest(); - request.setName(""); - - mockMvc.perform(post("/api/users") - .with(authenticatedAs(currentUser)) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isBadRequest()); - verify(userService, never()).createUser(any()); - } - @Test void createUser_whenEmailAlreadyTaken_shouldReturn400() throws Exception { when(userService.createUser(any())) From e1ec71e49db73da97b19b226743e18086b168e83 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Tue, 14 Apr 2026 11:02:40 +0200 Subject: [PATCH 56/85] make CI work --- .../org/example/vet1177/controller/UserControllerTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index c62ef3f8..8d07259a 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -3,6 +3,7 @@ import org.example.vet1177.dto.request.user.UserUpdateRequest; import org.example.vet1177.exception.BusinessRuleException; import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; import tools.jackson.databind.ObjectMapper; import org.example.vet1177.dto.request.user.UserRequest; @@ -32,8 +33,10 @@ import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + @WebMvcTest(UserController.class) @Import(SecurityConfig.class) +@ActiveProfiles("test") class UserControllerTest { @Autowired From df6375287b8c470087225a39f5e98dee584e1692 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Tue, 14 Apr 2026 12:54:11 +0200 Subject: [PATCH 57/85] fix: add missing security mocks to UserControllerTest --- .../vet1177/controller/PetControllerTest.java | 25 +++++-------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/src/test/java/org/example/vet1177/controller/PetControllerTest.java b/src/test/java/org/example/vet1177/controller/PetControllerTest.java index 32ae72b6..2564605e 100644 --- a/src/test/java/org/example/vet1177/controller/PetControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/PetControllerTest.java @@ -1,9 +1,6 @@ package org.example.vet1177.controller; //Används i commentControllerTest också, verkar funka där? -import org.example.vet1177.security.CustomUserDetailsService; -import org.example.vet1177.security.JwtService; -import org.springframework.test.context.ActiveProfiles; import tools.jackson.databind.ObjectMapper; import org.example.vet1177.exception.ResourceNotFoundException; import org.example.vet1177.dto.request.pet.PetRequest; @@ -38,7 +35,6 @@ @WebMvcTest(PetController.class) @Import(SecurityConfig.class) -@ActiveProfiles("test") public class PetControllerTest { @Autowired @@ -50,28 +46,25 @@ public class PetControllerTest { @MockitoBean private UserService userService; - @MockitoBean - private CustomUserDetailsService customUserDetailsService; - - @MockitoBean - private JwtService jwtService; - private User owner; private User vet; private Pet pet; private UUID ownerId; private UUID petId; - - @BeforeEach - void setUp() throws Exception { + void setUp() { ownerId = UUID.randomUUID(); petId = UUID.randomUUID(); owner = new User("Anna Ägare", "anna@mail.se", "hash", Role.OWNER); vet = new User("Dr. Erik Vet", "erik@vet.se", "hash", Role.VET); pet = new Pet(owner, "Molly", "Hund", "Labrador", LocalDate.of(2020, 1, 1), new BigDecimal("12.50")); } + private RequestPostProcessor authenticatedAs(User user) { + return authentication(new UsernamePasswordAuthenticationToken( + user, null, user.getAuthorities() + )); + } private PetRequest validPetRequest() { PetRequest request = new PetRequest(); @@ -83,12 +76,6 @@ private PetRequest validPetRequest() { return request; } - private RequestPostProcessor authenticatedAs(User user) { - return authentication(new UsernamePasswordAuthenticationToken( - user, null, user.getAuthorities() - )); - } - //POST /pets @Test void createPet_shouldReturn200WithPetResponse() throws Exception { From ddb4faf0173a3b5d5b08b0ad81128df2ce458053 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Mon, 13 Apr 2026 12:03:08 +0200 Subject: [PATCH 58/85] Test: GET /api/users and GET /api/users/{id} --- .../controller/UserControllerTest.java | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/test/java/org/example/vet1177/controller/UserControllerTest.java diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java new file mode 100644 index 00000000..36cddac0 --- /dev/null +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -0,0 +1,114 @@ +package org.example.vet1177.controller; + +import tools.jackson.databind.ObjectMapper; +import org.example.vet1177.dto.request.user.UserRequest; + +import org.example.vet1177.dto.response.user.UserResponse; +import org.example.vet1177.entities.Role; +import org.example.vet1177.entities.User; + +import org.example.vet1177.exception.ResourceNotFoundException; +import org.example.vet1177.security.SecurityConfig; +import org.example.vet1177.services.UserService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.context.annotation.Import; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.RequestPostProcessor; + +import java.util.List; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +@WebMvcTest(UserController.class) +@Import(SecurityConfig.class) +class UserControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockitoBean + private UserService userService; + + private User currentUser; + private UserResponse userResponse; + private UUID userId; + + @BeforeEach + void setUp() { + userId = UUID.randomUUID(); + currentUser = new User("Anna Karlsson", "anna@example.se", "hash", Role.ADMIN); + userResponse = new UserResponse(userId, "Anna Karlsson", "anna@example.se", Role.OWNER, null, null, null); + } + + private RequestPostProcessor authenticatedAs(User user) { + return authentication(new UsernamePasswordAuthenticationToken( + user, null, user.getAuthorities() + )); + } + + private UserRequest validUserRequest() { + UserRequest request = new UserRequest(); + request.setName("Anna Karlsson"); + request.setEmail("anna@example.se"); + request.setPassword("lösenord123"); + request.setRole(Role.OWNER); + return request; + } + + + // GET /api/users + + @Test + void getAllUsers_shouldReturn200WithList() throws Exception { + when(userService.getAllUsers()).thenReturn(List.of(userResponse)); + + mockMvc.perform(get("/api/users") + .with(authenticatedAs(currentUser))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].name").value("Anna Karlsson")); + } + + @Test + void getAllUsers_whenEmpty_shouldReturnEmptyList() throws Exception { + when(userService.getAllUsers()).thenReturn(List.of()); + + mockMvc.perform(get("/api/users") + .with(authenticatedAs(currentUser))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isEmpty()); + } + + // GET /api/users/{id} + + @Test + void getUserById_shouldReturn200WithUserResponse() throws Exception { + when(userService.getById(userId)).thenReturn(userResponse); + + mockMvc.perform(get("/api/users/{id}", userId) + .with(authenticatedAs(currentUser))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("Anna Karlsson")); + } + + @Test + void getUserById_whenNotFound_shouldReturn404() throws Exception { + when(userService.getById(any())) + .thenThrow(new ResourceNotFoundException("User", userId)); + + mockMvc.perform(get("/api/users/{id}", userId) + .with(authenticatedAs(currentUser))) + .andExpect(status().isNotFound()); + }} From 73fcee9c8ac237d5758bf9f36d5ce27cf73d62d7 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Mon, 13 Apr 2026 15:58:08 +0200 Subject: [PATCH 59/85] Test:POST /api/users, PUT /api/users/{id} --- .../controller/UserControllerTest.java | 97 ++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index 36cddac0..e6626da8 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -1,5 +1,8 @@ package org.example.vet1177.controller; +import org.example.vet1177.dto.request.user.UserUpdateRequest; +import org.example.vet1177.exception.BusinessRuleException; +import org.springframework.http.MediaType; import tools.jackson.databind.ObjectMapper; import org.example.vet1177.dto.request.user.UserRequest; @@ -111,4 +114,96 @@ void getUserById_whenNotFound_shouldReturn404() throws Exception { mockMvc.perform(get("/api/users/{id}", userId) .with(authenticatedAs(currentUser))) .andExpect(status().isNotFound()); - }} + } + + + // POST /api/users + + + @Test + void createUser_shouldReturn201WithUserResponse() throws Exception { + when(userService.createUser(any())).thenReturn(userResponse); + + mockMvc.perform(post("/api/users") + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(validUserRequest()))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.name").value("Anna Karlsson")); + } + + @Test + void createUser_whenInvalidRequest_shouldReturn400() throws Exception { + UserRequest request = validUserRequest(); + request.setEmail("inte-en-email"); + + mockMvc.perform(post("/api/users") + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + void createUser_whenEmailAlreadyTaken_shouldReturn400() throws Exception { + when(userService.createUser(any())) + .thenThrow(new BusinessRuleException("Email används redan")); + + mockMvc.perform(post("/api/users") + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(validUserRequest()))) + .andExpect(status().isBadRequest()); + } + + + // PUT /api/users/{id} + + + @Test + void updateUser_shouldReturn200WithUpdatedUserResponse() throws Exception { + UserResponse updated = new UserResponse(userId, "Uppdaterad Namn", "anna@example.se", Role.OWNER, null, null, null); + when(userService.updateUser(eq(userId), any())).thenReturn(updated); + + UserUpdateRequest request = new UserUpdateRequest(); + request.setName("Uppdaterad Namn"); + + mockMvc.perform(put("/api/users/{id}", userId) + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("Uppdaterad Namn")); + } + + @Test + void updateUser_whenNotFound_shouldReturn404() throws Exception { + when(userService.updateUser(any(), any())) + .thenThrow(new ResourceNotFoundException("User", userId)); + + UserUpdateRequest request = new UserUpdateRequest(); + request.setName("Uppdaterad Namn"); + + mockMvc.perform(put("/api/users/{id}", userId) + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()); + } + + @Test + void updateUser_whenBusinessRuleViolation_shouldReturn400() throws Exception { + when(userService.updateUser(any(), any())) + .thenThrow(new BusinessRuleException("Email används redan")); + + UserUpdateRequest request = new UserUpdateRequest(); + request.setEmail("tagen@example.se"); + + mockMvc.perform(put("/api/users/{id}", userId) + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + +} From 05337fcce4c951b2ac874e6142f867242d6d1d68 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Mon, 13 Apr 2026 17:17:23 +0200 Subject: [PATCH 60/85] Test: DELETE /api/users/{id} --- .../controller/UserControllerTest.java | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index e6626da8..a3090949 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -116,10 +116,7 @@ void getUserById_whenNotFound_shouldReturn404() throws Exception { .andExpect(status().isNotFound()); } - // POST /api/users - - @Test void createUser_shouldReturn201WithUserResponse() throws Exception { when(userService.createUser(any())).thenReturn(userResponse); @@ -206,4 +203,35 @@ void updateUser_whenBusinessRuleViolation_shouldReturn400() throws Exception { .andExpect(status().isBadRequest()); } + // DELETE /api/users/{id} + + @Test + void deleteUser_shouldReturn204() throws Exception { + doNothing().when(userService).deleteUser(userId); + + mockMvc.perform(delete("/api/users/{id}", userId) + .with(authenticatedAs(currentUser))) + .andExpect(status().isNoContent()); + } + + @Test + void deleteUser_whenNotFound_shouldReturn404() throws Exception { + doThrow(new ResourceNotFoundException("User", userId)) + .when(userService).deleteUser(any()); + + mockMvc.perform(delete("/api/users/{id}", userId) + .with(authenticatedAs(currentUser))) + .andExpect(status().isNotFound()); + } + + @Test + void deleteUser_whenHasLinkedResources_shouldReturn400() throws Exception { + doThrow(new BusinessRuleException("Användaren har kopplade djur och kan inte raderas")) + .when(userService).deleteUser(any()); + + mockMvc.perform(delete("/api/users/{id}", userId) + .with(authenticatedAs(currentUser))) + .andExpect(status().isBadRequest()); + } } + From ae7617207dbcb82c20a203a0444680867f50e019 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Tue, 14 Apr 2026 10:48:13 +0200 Subject: [PATCH 61/85] rabbit fixes --- .../vet1177/controller/UserControllerTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index a3090949..490cdcdc 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -139,6 +139,20 @@ void createUser_whenInvalidRequest_shouldReturn400() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isBadRequest()); + verify(userService, never()).createUser(any()); + } + + @Test + void createUser_whenNameMissing_shouldReturn400() throws Exception { + UserRequest request = validUserRequest(); + request.setName(""); + + mockMvc.perform(post("/api/users") + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + verify(userService, never()).createUser(any()); } @Test From c849dd9549b5b4a6fd4b9553d8dfbeb45f4964ff Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Tue, 14 Apr 2026 10:54:37 +0200 Subject: [PATCH 62/85] rabbit fixes --- .../vet1177/controller/UserControllerTest.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index 490cdcdc..c62ef3f8 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -142,19 +142,6 @@ void createUser_whenInvalidRequest_shouldReturn400() throws Exception { verify(userService, never()).createUser(any()); } - @Test - void createUser_whenNameMissing_shouldReturn400() throws Exception { - UserRequest request = validUserRequest(); - request.setName(""); - - mockMvc.perform(post("/api/users") - .with(authenticatedAs(currentUser)) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isBadRequest()); - verify(userService, never()).createUser(any()); - } - @Test void createUser_whenEmailAlreadyTaken_shouldReturn400() throws Exception { when(userService.createUser(any())) From 1c10da8b7dc280a2dc68fa75f544ad1aff97dbbb Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Tue, 14 Apr 2026 11:02:40 +0200 Subject: [PATCH 63/85] make CI work --- .../org/example/vet1177/controller/UserControllerTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index c62ef3f8..8d07259a 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -3,6 +3,7 @@ import org.example.vet1177.dto.request.user.UserUpdateRequest; import org.example.vet1177.exception.BusinessRuleException; import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; import tools.jackson.databind.ObjectMapper; import org.example.vet1177.dto.request.user.UserRequest; @@ -32,8 +33,10 @@ import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + @WebMvcTest(UserController.class) @Import(SecurityConfig.class) +@ActiveProfiles("test") class UserControllerTest { @Autowired From 132210d9599b71d5454c15586eef72fc2ae33e98 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Tue, 14 Apr 2026 11:39:49 +0200 Subject: [PATCH 64/85] fix: add missing security mocks to UserControllerTest --- .../org/example/vet1177/controller/UserControllerTest.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index 8d07259a..b12c3e4e 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -52,6 +52,12 @@ class UserControllerTest { private UserResponse userResponse; private UUID userId; + @MockitoBean + private org.example.vet1177.security.JwtService jwtService; + @MockitoBean + private org.example.vet1177.security.CustomUserDetailsService customUserDetailsService; + + @BeforeEach void setUp() { userId = UUID.randomUUID(); From 0748bca907eaf7014e6dda6f146c2a2295e2d0e4 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Mon, 13 Apr 2026 12:03:08 +0200 Subject: [PATCH 65/85] Test: GET /api/users and GET /api/users/{id} # Conflicts: # src/test/java/org/example/vet1177/controller/UserControllerTest.java --- .../controller/UserControllerTest.java | 134 +----------------- 1 file changed, 1 insertion(+), 133 deletions(-) diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index b12c3e4e..25e21a81 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -1,9 +1,5 @@ package org.example.vet1177.controller; -import org.example.vet1177.dto.request.user.UserUpdateRequest; -import org.example.vet1177.exception.BusinessRuleException; -import org.springframework.http.MediaType; -import org.springframework.test.context.ActiveProfiles; import tools.jackson.databind.ObjectMapper; import org.example.vet1177.dto.request.user.UserRequest; @@ -33,10 +29,8 @@ import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - @WebMvcTest(UserController.class) @Import(SecurityConfig.class) -@ActiveProfiles("test") class UserControllerTest { @Autowired @@ -52,12 +46,6 @@ class UserControllerTest { private UserResponse userResponse; private UUID userId; - @MockitoBean - private org.example.vet1177.security.JwtService jwtService; - @MockitoBean - private org.example.vet1177.security.CustomUserDetailsService customUserDetailsService; - - @BeforeEach void setUp() { userId = UUID.randomUUID(); @@ -124,124 +112,4 @@ void getUserById_whenNotFound_shouldReturn404() throws Exception { .with(authenticatedAs(currentUser))) .andExpect(status().isNotFound()); } - - // POST /api/users - @Test - void createUser_shouldReturn201WithUserResponse() throws Exception { - when(userService.createUser(any())).thenReturn(userResponse); - - mockMvc.perform(post("/api/users") - .with(authenticatedAs(currentUser)) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(validUserRequest()))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$.name").value("Anna Karlsson")); - } - - @Test - void createUser_whenInvalidRequest_shouldReturn400() throws Exception { - UserRequest request = validUserRequest(); - request.setEmail("inte-en-email"); - - mockMvc.perform(post("/api/users") - .with(authenticatedAs(currentUser)) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isBadRequest()); - verify(userService, never()).createUser(any()); - } - - @Test - void createUser_whenEmailAlreadyTaken_shouldReturn400() throws Exception { - when(userService.createUser(any())) - .thenThrow(new BusinessRuleException("Email används redan")); - - mockMvc.perform(post("/api/users") - .with(authenticatedAs(currentUser)) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(validUserRequest()))) - .andExpect(status().isBadRequest()); - } - - - // PUT /api/users/{id} - - - @Test - void updateUser_shouldReturn200WithUpdatedUserResponse() throws Exception { - UserResponse updated = new UserResponse(userId, "Uppdaterad Namn", "anna@example.se", Role.OWNER, null, null, null); - when(userService.updateUser(eq(userId), any())).thenReturn(updated); - - UserUpdateRequest request = new UserUpdateRequest(); - request.setName("Uppdaterad Namn"); - - mockMvc.perform(put("/api/users/{id}", userId) - .with(authenticatedAs(currentUser)) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.name").value("Uppdaterad Namn")); - } - - @Test - void updateUser_whenNotFound_shouldReturn404() throws Exception { - when(userService.updateUser(any(), any())) - .thenThrow(new ResourceNotFoundException("User", userId)); - - UserUpdateRequest request = new UserUpdateRequest(); - request.setName("Uppdaterad Namn"); - - mockMvc.perform(put("/api/users/{id}", userId) - .with(authenticatedAs(currentUser)) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isNotFound()); - } - - @Test - void updateUser_whenBusinessRuleViolation_shouldReturn400() throws Exception { - when(userService.updateUser(any(), any())) - .thenThrow(new BusinessRuleException("Email används redan")); - - UserUpdateRequest request = new UserUpdateRequest(); - request.setEmail("tagen@example.se"); - - mockMvc.perform(put("/api/users/{id}", userId) - .with(authenticatedAs(currentUser)) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isBadRequest()); - } - - // DELETE /api/users/{id} - - @Test - void deleteUser_shouldReturn204() throws Exception { - doNothing().when(userService).deleteUser(userId); - - mockMvc.perform(delete("/api/users/{id}", userId) - .with(authenticatedAs(currentUser))) - .andExpect(status().isNoContent()); - } - - @Test - void deleteUser_whenNotFound_shouldReturn404() throws Exception { - doThrow(new ResourceNotFoundException("User", userId)) - .when(userService).deleteUser(any()); - - mockMvc.perform(delete("/api/users/{id}", userId) - .with(authenticatedAs(currentUser))) - .andExpect(status().isNotFound()); - } - - @Test - void deleteUser_whenHasLinkedResources_shouldReturn400() throws Exception { - doThrow(new BusinessRuleException("Användaren har kopplade djur och kan inte raderas")) - .when(userService).deleteUser(any()); - - mockMvc.perform(delete("/api/users/{id}", userId) - .with(authenticatedAs(currentUser))) - .andExpect(status().isBadRequest()); - } -} - +} \ No newline at end of file From e4aa50c5bfea8770bddfbbe63fb4d7678dffd200 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Mon, 13 Apr 2026 15:58:08 +0200 Subject: [PATCH 66/85] Test:POST /api/users, PUT /api/users/{id} --- .../controller/UserControllerTest.java | 96 ++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index 25e21a81..e6626da8 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -1,5 +1,8 @@ package org.example.vet1177.controller; +import org.example.vet1177.dto.request.user.UserUpdateRequest; +import org.example.vet1177.exception.BusinessRuleException; +import org.springframework.http.MediaType; import tools.jackson.databind.ObjectMapper; import org.example.vet1177.dto.request.user.UserRequest; @@ -112,4 +115,95 @@ void getUserById_whenNotFound_shouldReturn404() throws Exception { .with(authenticatedAs(currentUser))) .andExpect(status().isNotFound()); } -} \ No newline at end of file + + + // POST /api/users + + + @Test + void createUser_shouldReturn201WithUserResponse() throws Exception { + when(userService.createUser(any())).thenReturn(userResponse); + + mockMvc.perform(post("/api/users") + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(validUserRequest()))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.name").value("Anna Karlsson")); + } + + @Test + void createUser_whenInvalidRequest_shouldReturn400() throws Exception { + UserRequest request = validUserRequest(); + request.setEmail("inte-en-email"); + + mockMvc.perform(post("/api/users") + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + void createUser_whenEmailAlreadyTaken_shouldReturn400() throws Exception { + when(userService.createUser(any())) + .thenThrow(new BusinessRuleException("Email används redan")); + + mockMvc.perform(post("/api/users") + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(validUserRequest()))) + .andExpect(status().isBadRequest()); + } + + + // PUT /api/users/{id} + + + @Test + void updateUser_shouldReturn200WithUpdatedUserResponse() throws Exception { + UserResponse updated = new UserResponse(userId, "Uppdaterad Namn", "anna@example.se", Role.OWNER, null, null, null); + when(userService.updateUser(eq(userId), any())).thenReturn(updated); + + UserUpdateRequest request = new UserUpdateRequest(); + request.setName("Uppdaterad Namn"); + + mockMvc.perform(put("/api/users/{id}", userId) + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("Uppdaterad Namn")); + } + + @Test + void updateUser_whenNotFound_shouldReturn404() throws Exception { + when(userService.updateUser(any(), any())) + .thenThrow(new ResourceNotFoundException("User", userId)); + + UserUpdateRequest request = new UserUpdateRequest(); + request.setName("Uppdaterad Namn"); + + mockMvc.perform(put("/api/users/{id}", userId) + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()); + } + + @Test + void updateUser_whenBusinessRuleViolation_shouldReturn400() throws Exception { + when(userService.updateUser(any(), any())) + .thenThrow(new BusinessRuleException("Email används redan")); + + UserUpdateRequest request = new UserUpdateRequest(); + request.setEmail("tagen@example.se"); + + mockMvc.perform(put("/api/users/{id}", userId) + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + +} From a1c0067566a831b290b695cbcc0c191d38ffd5f8 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Mon, 13 Apr 2026 17:17:23 +0200 Subject: [PATCH 67/85] Test: DELETE /api/users/{id} --- .../controller/UserControllerTest.java | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index e6626da8..a3090949 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -116,10 +116,7 @@ void getUserById_whenNotFound_shouldReturn404() throws Exception { .andExpect(status().isNotFound()); } - // POST /api/users - - @Test void createUser_shouldReturn201WithUserResponse() throws Exception { when(userService.createUser(any())).thenReturn(userResponse); @@ -206,4 +203,35 @@ void updateUser_whenBusinessRuleViolation_shouldReturn400() throws Exception { .andExpect(status().isBadRequest()); } + // DELETE /api/users/{id} + + @Test + void deleteUser_shouldReturn204() throws Exception { + doNothing().when(userService).deleteUser(userId); + + mockMvc.perform(delete("/api/users/{id}", userId) + .with(authenticatedAs(currentUser))) + .andExpect(status().isNoContent()); + } + + @Test + void deleteUser_whenNotFound_shouldReturn404() throws Exception { + doThrow(new ResourceNotFoundException("User", userId)) + .when(userService).deleteUser(any()); + + mockMvc.perform(delete("/api/users/{id}", userId) + .with(authenticatedAs(currentUser))) + .andExpect(status().isNotFound()); + } + + @Test + void deleteUser_whenHasLinkedResources_shouldReturn400() throws Exception { + doThrow(new BusinessRuleException("Användaren har kopplade djur och kan inte raderas")) + .when(userService).deleteUser(any()); + + mockMvc.perform(delete("/api/users/{id}", userId) + .with(authenticatedAs(currentUser))) + .andExpect(status().isBadRequest()); + } } + From e0cb6e13fd7590f4715d223f1cd89a7f2f387510 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Tue, 14 Apr 2026 10:48:13 +0200 Subject: [PATCH 68/85] rabbit fixes --- .../vet1177/controller/UserControllerTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index a3090949..490cdcdc 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -139,6 +139,20 @@ void createUser_whenInvalidRequest_shouldReturn400() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isBadRequest()); + verify(userService, never()).createUser(any()); + } + + @Test + void createUser_whenNameMissing_shouldReturn400() throws Exception { + UserRequest request = validUserRequest(); + request.setName(""); + + mockMvc.perform(post("/api/users") + .with(authenticatedAs(currentUser)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + verify(userService, never()).createUser(any()); } @Test From b2781e4eeaf5c12c433b92c7c13b1cc525bdb8e1 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Tue, 14 Apr 2026 10:54:37 +0200 Subject: [PATCH 69/85] rabbit fixes --- .../vet1177/controller/UserControllerTest.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index 490cdcdc..c62ef3f8 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -142,19 +142,6 @@ void createUser_whenInvalidRequest_shouldReturn400() throws Exception { verify(userService, never()).createUser(any()); } - @Test - void createUser_whenNameMissing_shouldReturn400() throws Exception { - UserRequest request = validUserRequest(); - request.setName(""); - - mockMvc.perform(post("/api/users") - .with(authenticatedAs(currentUser)) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isBadRequest()); - verify(userService, never()).createUser(any()); - } - @Test void createUser_whenEmailAlreadyTaken_shouldReturn400() throws Exception { when(userService.createUser(any())) From 3a78cf1f8bc4248aeff61e927b2bb87c033bf89a Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Tue, 14 Apr 2026 11:02:40 +0200 Subject: [PATCH 70/85] make CI work --- .../org/example/vet1177/controller/UserControllerTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index c62ef3f8..8d07259a 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -3,6 +3,7 @@ import org.example.vet1177.dto.request.user.UserUpdateRequest; import org.example.vet1177.exception.BusinessRuleException; import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; import tools.jackson.databind.ObjectMapper; import org.example.vet1177.dto.request.user.UserRequest; @@ -32,8 +33,10 @@ import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + @WebMvcTest(UserController.class) @Import(SecurityConfig.class) +@ActiveProfiles("test") class UserControllerTest { @Autowired From 40dd39514c91c0a06469c5daa316d3655f0ab8b1 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Tue, 14 Apr 2026 12:54:11 +0200 Subject: [PATCH 71/85] fix: add missing security mocks to UserControllerTest --- .../vet1177/controller/PetControllerTest.java | 25 +++++-------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/src/test/java/org/example/vet1177/controller/PetControllerTest.java b/src/test/java/org/example/vet1177/controller/PetControllerTest.java index 32ae72b6..2564605e 100644 --- a/src/test/java/org/example/vet1177/controller/PetControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/PetControllerTest.java @@ -1,9 +1,6 @@ package org.example.vet1177.controller; //Används i commentControllerTest också, verkar funka där? -import org.example.vet1177.security.CustomUserDetailsService; -import org.example.vet1177.security.JwtService; -import org.springframework.test.context.ActiveProfiles; import tools.jackson.databind.ObjectMapper; import org.example.vet1177.exception.ResourceNotFoundException; import org.example.vet1177.dto.request.pet.PetRequest; @@ -38,7 +35,6 @@ @WebMvcTest(PetController.class) @Import(SecurityConfig.class) -@ActiveProfiles("test") public class PetControllerTest { @Autowired @@ -50,28 +46,25 @@ public class PetControllerTest { @MockitoBean private UserService userService; - @MockitoBean - private CustomUserDetailsService customUserDetailsService; - - @MockitoBean - private JwtService jwtService; - private User owner; private User vet; private Pet pet; private UUID ownerId; private UUID petId; - - @BeforeEach - void setUp() throws Exception { + void setUp() { ownerId = UUID.randomUUID(); petId = UUID.randomUUID(); owner = new User("Anna Ägare", "anna@mail.se", "hash", Role.OWNER); vet = new User("Dr. Erik Vet", "erik@vet.se", "hash", Role.VET); pet = new Pet(owner, "Molly", "Hund", "Labrador", LocalDate.of(2020, 1, 1), new BigDecimal("12.50")); } + private RequestPostProcessor authenticatedAs(User user) { + return authentication(new UsernamePasswordAuthenticationToken( + user, null, user.getAuthorities() + )); + } private PetRequest validPetRequest() { PetRequest request = new PetRequest(); @@ -83,12 +76,6 @@ private PetRequest validPetRequest() { return request; } - private RequestPostProcessor authenticatedAs(User user) { - return authentication(new UsernamePasswordAuthenticationToken( - user, null, user.getAuthorities() - )); - } - //POST /pets @Test void createPet_shouldReturn200WithPetResponse() throws Exception { From 2ff5a8095d35b0ab9e70c81f9b436f8d7a767496 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Tue, 14 Apr 2026 13:23:38 +0200 Subject: [PATCH 72/85] fix: UserControllerTest --- .../vet1177/controller/PetControllerTest.java | 25 ++++++++++++++----- .../controller/UserControllerTest.java | 8 ++++-- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/test/java/org/example/vet1177/controller/PetControllerTest.java b/src/test/java/org/example/vet1177/controller/PetControllerTest.java index 2564605e..32ae72b6 100644 --- a/src/test/java/org/example/vet1177/controller/PetControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/PetControllerTest.java @@ -1,6 +1,9 @@ package org.example.vet1177.controller; //Används i commentControllerTest också, verkar funka där? +import org.example.vet1177.security.CustomUserDetailsService; +import org.example.vet1177.security.JwtService; +import org.springframework.test.context.ActiveProfiles; import tools.jackson.databind.ObjectMapper; import org.example.vet1177.exception.ResourceNotFoundException; import org.example.vet1177.dto.request.pet.PetRequest; @@ -35,6 +38,7 @@ @WebMvcTest(PetController.class) @Import(SecurityConfig.class) +@ActiveProfiles("test") public class PetControllerTest { @Autowired @@ -46,25 +50,28 @@ public class PetControllerTest { @MockitoBean private UserService userService; + @MockitoBean + private CustomUserDetailsService customUserDetailsService; + + @MockitoBean + private JwtService jwtService; + private User owner; private User vet; private Pet pet; private UUID ownerId; private UUID petId; + + @BeforeEach - void setUp() { + void setUp() throws Exception { ownerId = UUID.randomUUID(); petId = UUID.randomUUID(); owner = new User("Anna Ägare", "anna@mail.se", "hash", Role.OWNER); vet = new User("Dr. Erik Vet", "erik@vet.se", "hash", Role.VET); pet = new Pet(owner, "Molly", "Hund", "Labrador", LocalDate.of(2020, 1, 1), new BigDecimal("12.50")); } - private RequestPostProcessor authenticatedAs(User user) { - return authentication(new UsernamePasswordAuthenticationToken( - user, null, user.getAuthorities() - )); - } private PetRequest validPetRequest() { PetRequest request = new PetRequest(); @@ -76,6 +83,12 @@ private PetRequest validPetRequest() { return request; } + private RequestPostProcessor authenticatedAs(User user) { + return authentication(new UsernamePasswordAuthenticationToken( + user, null, user.getAuthorities() + )); + } + //POST /pets @Test void createPet_shouldReturn200WithPetResponse() throws Exception { diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index 8d07259a..c63cbb94 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -3,7 +3,6 @@ import org.example.vet1177.dto.request.user.UserUpdateRequest; import org.example.vet1177.exception.BusinessRuleException; import org.springframework.http.MediaType; -import org.springframework.test.context.ActiveProfiles; import tools.jackson.databind.ObjectMapper; import org.example.vet1177.dto.request.user.UserRequest; @@ -36,7 +35,6 @@ @WebMvcTest(UserController.class) @Import(SecurityConfig.class) -@ActiveProfiles("test") class UserControllerTest { @Autowired @@ -48,6 +46,12 @@ class UserControllerTest { @MockitoBean private UserService userService; + @MockitoBean + private org.example.vet1177.security.JwtService jwtService; + + @MockitoBean + private org.example.vet1177.security.CustomUserDetailsService customUserDetailsService; + private User currentUser; private UserResponse userResponse; private UUID userId; From 01d1ef6e7ca55fbb936da69c82f10f251d218619 Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Tue, 14 Apr 2026 13:41:20 +0200 Subject: [PATCH 73/85] feat: add AuthRequest DTO for authentication requests - Introduced `AuthRequest` record with email and password fields. - Added validation annotations to ensure required fields are provided. --- .../example/vet1177/security/auth/dto/AuthRequest.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/main/java/org/example/vet1177/security/auth/dto/AuthRequest.java diff --git a/src/main/java/org/example/vet1177/security/auth/dto/AuthRequest.java b/src/main/java/org/example/vet1177/security/auth/dto/AuthRequest.java new file mode 100644 index 00000000..7c314c4f --- /dev/null +++ b/src/main/java/org/example/vet1177/security/auth/dto/AuthRequest.java @@ -0,0 +1,8 @@ +package org.example.vet1177.security.auth.dto; + +import jakarta.validation.constraints.NotBlank; + +public record AuthRequest( + @NotBlank(message = "Email måste anges") String email, + @NotBlank(message = "Lösenord måste anges") String password +) {} From 45cb16056d871ee0a6bf1fb601de61c70c5d59a0 Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Tue, 14 Apr 2026 13:41:35 +0200 Subject: [PATCH 74/85] feat: add RegisterRequest DTO for user registration - Created `RegisterRequest` record with fields for name, email, password, and role. - Added validation annotations to ensure all required fields are provided with appropriate constraints. --- .../vet1177/security/auth/dto/RegisterRequest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/main/java/org/example/vet1177/security/auth/dto/RegisterRequest.java diff --git a/src/main/java/org/example/vet1177/security/auth/dto/RegisterRequest.java b/src/main/java/org/example/vet1177/security/auth/dto/RegisterRequest.java new file mode 100644 index 00000000..72392314 --- /dev/null +++ b/src/main/java/org/example/vet1177/security/auth/dto/RegisterRequest.java @@ -0,0 +1,14 @@ +package org.example.vet1177.security.auth.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import org.example.vet1177.entities.Role; + +public record RegisterRequest( + @NotBlank(message = "Namn måste anges") String name, + @NotBlank(message = "Email måste anges") @Email(message = "Ogiltig emailadress") String email, + @NotBlank(message = "Lösenord måste anges") @Size(min = 8, message = "Lösenord måste vara minst 8 tecken") String password, + @NotNull(message = "Roll måste anges") Role role +) {} From e21c397f6691ce97e71715dcb4ad98d547096163 Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Tue, 14 Apr 2026 13:41:52 +0200 Subject: [PATCH 75/85] feat: add AuthResponse DTO for authentication responses - Introduced `AuthResponse` record with fields for token, userId, name, email, and role. --- .../vet1177/security/auth/dto/AuthResponse.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/example/vet1177/security/auth/dto/AuthResponse.java b/src/main/java/org/example/vet1177/security/auth/dto/AuthResponse.java index 3ce1928d..34c4eaee 100644 --- a/src/main/java/org/example/vet1177/security/auth/dto/AuthResponse.java +++ b/src/main/java/org/example/vet1177/security/auth/dto/AuthResponse.java @@ -1,4 +1,13 @@ package org.example.vet1177.security.auth.dto; -public class AuthResponse { -} +import org.example.vet1177.entities.Role; + +import java.util.UUID; + +public record AuthResponse( + String token, + UUID userId, + String name, + String email, + Role role +) {} From 38c2e37cd9744d3c729983efb55cb4288e7b1de4 Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Tue, 14 Apr 2026 13:49:22 +0200 Subject: [PATCH 76/85] refactor: remove role field from RegisterRequest DTO --- .../example/vet1177/security/auth/dto/RegisterRequest.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/org/example/vet1177/security/auth/dto/RegisterRequest.java b/src/main/java/org/example/vet1177/security/auth/dto/RegisterRequest.java index 72392314..9292d489 100644 --- a/src/main/java/org/example/vet1177/security/auth/dto/RegisterRequest.java +++ b/src/main/java/org/example/vet1177/security/auth/dto/RegisterRequest.java @@ -2,13 +2,10 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; -import org.example.vet1177.entities.Role; public record RegisterRequest( @NotBlank(message = "Namn måste anges") String name, @NotBlank(message = "Email måste anges") @Email(message = "Ogiltig emailadress") String email, - @NotBlank(message = "Lösenord måste anges") @Size(min = 8, message = "Lösenord måste vara minst 8 tecken") String password, - @NotNull(message = "Roll måste anges") Role role + @NotBlank(message = "Lösenord måste anges") @Size(min = 8, message = "Lösenord måste vara minst 8 tecken") String password ) {} From 0c4686ee0ac4425b036b2d96161254dbfa8d2412 Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Tue, 14 Apr 2026 13:50:15 +0200 Subject: [PATCH 77/85] feat: enhance AuthRequest and RegisterRequest DTOs with improved validation and password protection - Added `@Email` annotation in `AuthRequest` for email validation. - Introduced `@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)` for password fields to ensure write-only access. - Overrode `toString()` to mask sensitive password data in logs. --- .../vet1177/security/auth/dto/AuthRequest.java | 13 ++++++++++--- .../vet1177/security/auth/dto/RegisterRequest.java | 10 ++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/example/vet1177/security/auth/dto/AuthRequest.java b/src/main/java/org/example/vet1177/security/auth/dto/AuthRequest.java index 7c314c4f..d8222069 100644 --- a/src/main/java/org/example/vet1177/security/auth/dto/AuthRequest.java +++ b/src/main/java/org/example/vet1177/security/auth/dto/AuthRequest.java @@ -1,8 +1,15 @@ package org.example.vet1177.security.auth.dto; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; public record AuthRequest( - @NotBlank(message = "Email måste anges") String email, - @NotBlank(message = "Lösenord måste anges") String password -) {} + @NotBlank(message = "Email måste anges") @Email(message = "Ogiltig emailadress") String email, + @NotBlank(message = "Lösenord måste anges") @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) String password +) { + @Override + public String toString() { + return "AuthRequest[email=" + email + ", password=***]"; + } +} diff --git a/src/main/java/org/example/vet1177/security/auth/dto/RegisterRequest.java b/src/main/java/org/example/vet1177/security/auth/dto/RegisterRequest.java index 9292d489..3bc92ca4 100644 --- a/src/main/java/org/example/vet1177/security/auth/dto/RegisterRequest.java +++ b/src/main/java/org/example/vet1177/security/auth/dto/RegisterRequest.java @@ -1,5 +1,6 @@ package org.example.vet1177.security.auth.dto; +import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; @@ -7,5 +8,10 @@ public record RegisterRequest( @NotBlank(message = "Namn måste anges") String name, @NotBlank(message = "Email måste anges") @Email(message = "Ogiltig emailadress") String email, - @NotBlank(message = "Lösenord måste anges") @Size(min = 8, message = "Lösenord måste vara minst 8 tecken") String password -) {} + @NotBlank(message = "Lösenord måste anges") @Size(min = 8, message = "Lösenord måste vara minst 8 tecken") @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) String password +) { + @Override + public String toString() { + return "RegisterRequest[name=" + name + ", email=" + email + ", password=***]"; + } +} From dfed255249519d886fd4f990540bbdc0f4deabf9 Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Tue, 14 Apr 2026 13:51:42 +0200 Subject: [PATCH 78/85] Minor fix --- .../java/org/example/vet1177/security/auth/dto/AuthRequest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/example/vet1177/security/auth/dto/AuthRequest.java b/src/main/java/org/example/vet1177/security/auth/dto/AuthRequest.java index d8222069..e9e4765b 100644 --- a/src/main/java/org/example/vet1177/security/auth/dto/AuthRequest.java +++ b/src/main/java/org/example/vet1177/security/auth/dto/AuthRequest.java @@ -4,6 +4,7 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; + public record AuthRequest( @NotBlank(message = "Email måste anges") @Email(message = "Ogiltig emailadress") String email, @NotBlank(message = "Lösenord måste anges") @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) String password From fc0b4392ba09fef64ac6c23c2f9ea4d82e8930da Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Tue, 14 Apr 2026 15:04:38 +0200 Subject: [PATCH 79/85] feat: add admin-only search user by email endpoint #132 --- .../org/example/vet1177/controller/UserController.java | 10 ++++++++++ .../org/example/vet1177/security/SecurityConfig.java | 3 +++ .../java/org/example/vet1177/services/UserService.java | 8 ++++++++ 3 files changed, 21 insertions(+) diff --git a/src/main/java/org/example/vet1177/controller/UserController.java b/src/main/java/org/example/vet1177/controller/UserController.java index 3c227e67..6dfb0dd5 100644 --- a/src/main/java/org/example/vet1177/controller/UserController.java +++ b/src/main/java/org/example/vet1177/controller/UserController.java @@ -10,6 +10,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.example.vet1177.dto.request.user.UserUpdateRequest; +import org.springframework.security.access.prepost.PreAuthorize; import java.util.List; import java.util.UUID; @@ -42,6 +43,15 @@ public ResponseEntity getUserById(@PathVariable UUID id) { return ResponseEntity.ok(user); } + // GET /users/search?email= - Sök användare på email (endast admin) + @GetMapping("/search") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity searchByEmail(@RequestParam String email) { + log.info("GET /api/users/search?email={}", email); + UserResponse user = userService.searchByEmail(email); + return ResponseEntity.ok(user); + } + //POST /users - skapa ny användare @PostMapping public ResponseEntity createUser(@Valid @RequestBody UserRequest request) { diff --git a/src/main/java/org/example/vet1177/security/SecurityConfig.java b/src/main/java/org/example/vet1177/security/SecurityConfig.java index 377048b2..638cda8b 100644 --- a/src/main/java/org/example/vet1177/security/SecurityConfig.java +++ b/src/main/java/org/example/vet1177/security/SecurityConfig.java @@ -7,6 +7,7 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -27,6 +28,8 @@ * läsa jwt.secret-key och jwt.expiration-ms från application.properties och * skapa en JwtProperties-bean som kan injiceras i JwtService. */ + +@EnableMethodSecurity @Configuration @EnableConfigurationProperties(JwtProperties.class) public class SecurityConfig { diff --git a/src/main/java/org/example/vet1177/services/UserService.java b/src/main/java/org/example/vet1177/services/UserService.java index f972c092..f98d0026 100644 --- a/src/main/java/org/example/vet1177/services/UserService.java +++ b/src/main/java/org/example/vet1177/services/UserService.java @@ -71,6 +71,14 @@ public User getByEmail(String email){ .orElseThrow(() -> new ResourceNotFoundException("User", email)); } + // GET /users/search?email= - Returnerar UserResponse DTO till controller (admin only) + public UserResponse searchByEmail(String email) { + log.debug("Searching user by email={}", email); + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new ResourceNotFoundException("User", email)); + return mapToResponse(user); + } + // Returnerar User-entiteten, används internt när andra services behöver ett User-objekt. OK? - annars public User getUserEntityById(UUID id) { log.debug("Fetching user entity id={}", id); From 49c4da76de3970c910c24fc74cdf505acbdaf843 Mon Sep 17 00:00:00 2001 From: Linda Eskilsson Date: Tue, 14 Apr 2026 15:10:43 +0200 Subject: [PATCH 80/85] fix: remove email from log to avoid PII exposure --- .../java/org/example/vet1177/controller/UserController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/example/vet1177/controller/UserController.java b/src/main/java/org/example/vet1177/controller/UserController.java index 6dfb0dd5..b4fd2cfb 100644 --- a/src/main/java/org/example/vet1177/controller/UserController.java +++ b/src/main/java/org/example/vet1177/controller/UserController.java @@ -47,7 +47,7 @@ public ResponseEntity getUserById(@PathVariable UUID id) { @GetMapping("/search") @PreAuthorize("hasRole('ADMIN')") public ResponseEntity searchByEmail(@RequestParam String email) { - log.info("GET /api/users/search?email={}", email); + log.info("GET /api/users/search"); UserResponse user = userService.searchByEmail(email); return ResponseEntity.ok(user); } From bcbab8e472c35c2f85188bab54da94546203821b Mon Sep 17 00:00:00 2001 From: Tatjana Date: Sun, 12 Apr 2026 22:23:47 +0200 Subject: [PATCH 81/85] Test: integration tests for vet --- .../integration/vet/VetIntegrationTest.java | 302 ++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 src/test/java/org/example/vet1177/integration/vet/VetIntegrationTest.java diff --git a/src/test/java/org/example/vet1177/integration/vet/VetIntegrationTest.java b/src/test/java/org/example/vet1177/integration/vet/VetIntegrationTest.java new file mode 100644 index 00000000..bda03995 --- /dev/null +++ b/src/test/java/org/example/vet1177/integration/vet/VetIntegrationTest.java @@ -0,0 +1,302 @@ +package org.example.vet1177.integration.vet; + +import org.example.vet1177.config.AwsS3Properties; +import org.example.vet1177.entities.Clinic; +import org.example.vet1177.entities.Role; +import org.example.vet1177.entities.User; +import org.example.vet1177.entities.Vet; +import org.example.vet1177.exception.ForbiddenException; +import org.example.vet1177.integration.TestDataFactory; +import org.example.vet1177.policy.AdminPolicy; +import org.example.vet1177.repository.ClinicRepository; +import org.example.vet1177.repository.UserRepository; +import org.example.vet1177.repository.VetRepository; +import org.example.vet1177.services.FileStorageService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc(addFilters = false) +@ActiveProfiles("test") +@TestPropertySource(properties = { + "spring.datasource.url=jdbc:h2:mem:testdb;MODE=PostgreSQL", + "spring.datasource.driver-class-name=org.h2.Driver" +}) +class VetIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private VetRepository vetRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ClinicRepository clinicRepository; + + @MockitoBean + private FileStorageService fileStorageService; + + @MockitoBean + private AwsS3Properties awsS3Properties; + + @MockitoBean + private AdminPolicy adminPolicy; + + @BeforeEach + void setUp() { + vetRepository.deleteAll(); + userRepository.deleteAll(); + clinicRepository.deleteAll(); + } + + @BeforeEach + void resetMocks() { + reset(adminPolicy); + } + + private User createAdmin(Clinic clinic) { + User admin = new User( + "Admin", + UUID.randomUUID() + "@test.com", + "password123", + Role.ADMIN, + clinic + ); + return userRepository.save(admin); + } + + private User createVetUser(Clinic clinic) { + User vetUser = new User( + "Vet User", + UUID.randomUUID() + "@test.com", + "password123", + Role.VET, + clinic + ); + return userRepository.save(vetUser); + } + + private UsernamePasswordAuthenticationToken auth(User user) { + return new UsernamePasswordAuthenticationToken(user, null, List.of()); + } + + @Test + void VT_P0_01_admin_can_create_vet() throws Exception { + doNothing().when(adminPolicy).requireAdmin(any()); + Clinic clinic = TestDataFactory.createClinic(clinicRepository); + User admin = createAdmin(clinic); + User targetUser = TestDataFactory.createOwner(userRepository, clinic); + + String body = """ + { + "userId": "%s", + "licenseId": "LIC-1001", + "specialization": "Surgery", + "bookingInfo": "Weekdays only" + } + """.formatted(targetUser.getId()); + + mockMvc.perform(post("/api/vets") + .with(authentication(auth(admin))) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.userId").value(targetUser.getId().toString())) + .andExpect(jsonPath("$.licenseId").value("LIC-1001")) + .andExpect(jsonPath("$.specialization").value("Surgery")) + .andExpect(jsonPath("$.bookingInfo").value("Weekdays only")); + + assertEquals(1, vetRepository.count()); + + User updatedUser = userRepository.findById(targetUser.getId()).orElseThrow(); + assertEquals(Role.VET, updatedUser.getRole()); + } + // TODO: Enable when real authentication is implemented +// @Test +// void VT_P0_02_non_admin_cannot_create_vet_as_vet() throws Exception { +// Clinic clinic = TestDataFactory.createClinic(clinicRepository); +// User nonAdminVet = createVetUser(clinic); +// User targetUser = TestDataFactory.createOwner(userRepository, clinic); +// +// String body = """ +// { +// "userId": "%s", +// "licenseId": "LIC-1003", +// "specialization": "Cardiology", +// "bookingInfo": "Tue-Thu" +// } +// """.formatted(targetUser.getId()); +// +// mockMvc.perform(post("/api/vets") +// .with(authentication(auth(nonAdminVet))) +// .contentType(MediaType.APPLICATION_JSON) +// .content(body)) +// .andExpect(status().isForbidden()); +// +// assertEquals(0, vetRepository.count()); +// } + + @Test + void VT_P0_03_duplicate_license_id_rejected() throws Exception { + Clinic clinic = TestDataFactory.createClinic(clinicRepository); + User admin = createAdmin(clinic); + + User firstUser = TestDataFactory.createOwner(userRepository, clinic); + User secondUser = TestDataFactory.createOwner(userRepository, clinic); + + Vet existingVet = new Vet(firstUser, "LIC-DUP-1", "Surgery", "Morning"); + vetRepository.save(existingVet); + + String body = """ + { + "userId": "%s", + "licenseId": "LIC-DUP-1", + "specialization": "Dentistry", + "bookingInfo": "Afternoon" + } + """.formatted(secondUser.getId()); + + mockMvc.perform(post("/api/vets") + .with(authentication(auth(admin))) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isBadRequest()); + + assertEquals(1, vetRepository.count()); + assertTrue(vetRepository.existsByLicenseId("LIC-DUP-1")); + } + + @Test + void VT_P0_04_missing_target_user_rejected() throws Exception { + Clinic clinic = TestDataFactory.createClinic(clinicRepository); + User admin = createAdmin(clinic); + + String body = """ + { + "userId": "%s", + "licenseId": "LIC-404", + "specialization": "Neurology", + "bookingInfo": "By appointment" + } + """.formatted(UUID.randomUUID()); + + mockMvc.perform(post("/api/vets") + .with(authentication(auth(admin))) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isNotFound()); + + assertEquals(0, vetRepository.count()); + } + + @Test + void VT_P0_05_get_all_vets_includes_clinic_name() throws Exception { + Clinic clinic = TestDataFactory.createClinic(clinicRepository); + User user = createVetUser(clinic); + + Vet vet = new Vet(user, "LIC-GET-ALL", "Orthopedics", "Mon-Wed"); + vetRepository.save(vet); + + mockMvc.perform(get("/api/vets")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].userId").value(user.getId().toString())) + .andExpect(jsonPath("$[0].licenseId").value("LIC-GET-ALL")) + .andExpect(jsonPath("$[0].clinicName").value(clinic.getName())); + } + + @Test + void VT_P0_06_get_vet_by_id_returns_expected_vet() throws Exception { + Clinic clinic = TestDataFactory.createClinic(clinicRepository); + User user = createVetUser(clinic); + + Vet vet = new Vet(user, "LIC-BY-ID", "Internal medicine", "Fri"); + vetRepository.save(vet); + + mockMvc.perform(get("/api/vets/" + user.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.userId").value(user.getId().toString())) + .andExpect(jsonPath("$.licenseId").value("LIC-BY-ID")) + .andExpect(jsonPath("$.clinicName").value(clinic.getName())); + } + + @Test + void VT_P0_06_get_vet_by_id_returns_404_for_unknown_id() throws Exception { + mockMvc.perform(get("/api/vets/" + UUID.randomUUID())) + .andExpect(status().isNotFound()); + } + + @Test + void VT_P1_01_validation_error_for_invalid_vet_request() throws Exception { + Clinic clinic = TestDataFactory.createClinic(clinicRepository); + User admin = createAdmin(clinic); + + String body = """ + { + "userId": null, + "licenseId": "", + "specialization": "Valid specialization", + "bookingInfo": "Valid booking info" + } + """; + + mockMvc.perform(post("/api/vets") + .with(authentication(auth(admin))) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isBadRequest()); + + assertEquals(0, vetRepository.count()); + } + + @Test + void VT_P1_02_existing_vet_role_remains_stable() throws Exception { + Clinic clinic = TestDataFactory.createClinic(clinicRepository); + User admin = createAdmin(clinic); + User targetUser = createVetUser(clinic); // redan VET, men ännu ingen vet_details-rad + + String body = """ + { + "userId": "%s", + "licenseId": "LIC-STABLE", + "specialization": "Exotics", + "bookingInfo": "Weekends" + } + """.formatted(targetUser.getId()); + + mockMvc.perform(post("/api/vets") + .with(authentication(auth(admin))) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.userId").value(targetUser.getId().toString())) + .andExpect(jsonPath("$.licenseId").value("LIC-STABLE")); + + User reloadedUser = userRepository.findById(targetUser.getId()).orElseThrow(); + assertEquals(Role.VET, reloadedUser.getRole()); + assertEquals(1, vetRepository.count()); + } +} \ No newline at end of file From 5f7d3935b8329a07283896e5f887490a155333e8 Mon Sep 17 00:00:00 2001 From: Tatjana Date: Tue, 14 Apr 2026 21:54:21 +0200 Subject: [PATCH 82/85] Test: improve integration tests for vet --- .../integration/vet/VetIntegrationTest.java | 67 +++++++++++-------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/src/test/java/org/example/vet1177/integration/vet/VetIntegrationTest.java b/src/test/java/org/example/vet1177/integration/vet/VetIntegrationTest.java index bda03995..840c464f 100644 --- a/src/test/java/org/example/vet1177/integration/vet/VetIntegrationTest.java +++ b/src/test/java/org/example/vet1177/integration/vet/VetIntegrationTest.java @@ -37,7 +37,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest -@AutoConfigureMockMvc(addFilters = false) +@AutoConfigureMockMvc @ActiveProfiles("test") @TestPropertySource(properties = { "spring.datasource.url=jdbc:h2:mem:testdb;MODE=PostgreSQL", @@ -134,31 +134,36 @@ void VT_P0_01_admin_can_create_vet() throws Exception { User updatedUser = userRepository.findById(targetUser.getId()).orElseThrow(); assertEquals(Role.VET, updatedUser.getRole()); + + verify(adminPolicy).requireAdmin(admin); + } + @Test + void VT_P0_02_non_admin_cannot_create_vet_as_vet() throws Exception { + Clinic clinic = TestDataFactory.createClinic(clinicRepository); + User nonAdminVet = createVetUser(clinic); + User targetUser = TestDataFactory.createOwner(userRepository, clinic); + + doThrow(new ForbiddenException("Åtkomst nekad: Endast administratörer har behörighet")) + .when(adminPolicy).requireAdmin(any()); + + String body = """ + { + "userId": "%s", + "licenseId": "LIC-1003", + "specialization": "Cardiology", + "bookingInfo": "Tue-Thu" + } + """.formatted(targetUser.getId()); + + mockMvc.perform(post("/api/vets") + .with(authentication(auth(nonAdminVet))) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isForbidden()); + + verify(adminPolicy, times(1)).requireAdmin(any()); + assertEquals(0, vetRepository.count()); } - // TODO: Enable when real authentication is implemented -// @Test -// void VT_P0_02_non_admin_cannot_create_vet_as_vet() throws Exception { -// Clinic clinic = TestDataFactory.createClinic(clinicRepository); -// User nonAdminVet = createVetUser(clinic); -// User targetUser = TestDataFactory.createOwner(userRepository, clinic); -// -// String body = """ -// { -// "userId": "%s", -// "licenseId": "LIC-1003", -// "specialization": "Cardiology", -// "bookingInfo": "Tue-Thu" -// } -// """.formatted(targetUser.getId()); -// -// mockMvc.perform(post("/api/vets") -// .with(authentication(auth(nonAdminVet))) -// .contentType(MediaType.APPLICATION_JSON) -// .content(body)) -// .andExpect(status().isForbidden()); -// -// assertEquals(0, vetRepository.count()); -// } @Test void VT_P0_03_duplicate_license_id_rejected() throws Exception { @@ -221,7 +226,8 @@ void VT_P0_05_get_all_vets_includes_clinic_name() throws Exception { Vet vet = new Vet(user, "LIC-GET-ALL", "Orthopedics", "Mon-Wed"); vetRepository.save(vet); - mockMvc.perform(get("/api/vets")) + mockMvc.perform(get("/api/vets") + .with(authentication(auth(user)))) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].userId").value(user.getId().toString())) .andExpect(jsonPath("$[0].licenseId").value("LIC-GET-ALL")) @@ -236,7 +242,8 @@ void VT_P0_06_get_vet_by_id_returns_expected_vet() throws Exception { Vet vet = new Vet(user, "LIC-BY-ID", "Internal medicine", "Fri"); vetRepository.save(vet); - mockMvc.perform(get("/api/vets/" + user.getId())) + mockMvc.perform(get("/api/vets/" + user.getId()) + .with(authentication(auth(user)))) .andExpect(status().isOk()) .andExpect(jsonPath("$.userId").value(user.getId().toString())) .andExpect(jsonPath("$.licenseId").value("LIC-BY-ID")) @@ -245,7 +252,11 @@ void VT_P0_06_get_vet_by_id_returns_expected_vet() throws Exception { @Test void VT_P0_06_get_vet_by_id_returns_404_for_unknown_id() throws Exception { - mockMvc.perform(get("/api/vets/" + UUID.randomUUID())) + Clinic clinic = TestDataFactory.createClinic(clinicRepository); + User user = createVetUser(clinic); + + mockMvc.perform(get("/api/vets/" + UUID.randomUUID()) + .with(authentication(auth(user)))) .andExpect(status().isNotFound()); } From dc8d7105acb36c5ae3563eebb05e483cba5abfc7 Mon Sep 17 00:00:00 2001 From: Tatjana Date: Tue, 14 Apr 2026 22:08:11 +0200 Subject: [PATCH 83/85] Test: improve integration tests for vet --- .../org/example/vet1177/integration/vet/VetIntegrationTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/org/example/vet1177/integration/vet/VetIntegrationTest.java b/src/test/java/org/example/vet1177/integration/vet/VetIntegrationTest.java index 8dccdeee..840c464f 100644 --- a/src/test/java/org/example/vet1177/integration/vet/VetIntegrationTest.java +++ b/src/test/java/org/example/vet1177/integration/vet/VetIntegrationTest.java @@ -226,6 +226,8 @@ void VT_P0_05_get_all_vets_includes_clinic_name() throws Exception { Vet vet = new Vet(user, "LIC-GET-ALL", "Orthopedics", "Mon-Wed"); vetRepository.save(vet); + mockMvc.perform(get("/api/vets") + .with(authentication(auth(user)))) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].userId").value(user.getId().toString())) .andExpect(jsonPath("$[0].licenseId").value("LIC-GET-ALL")) From 4235cfecec5d9db9f4c100f47761b46219df580e Mon Sep 17 00:00:00 2001 From: Tatjana Date: Tue, 14 Apr 2026 22:40:46 +0200 Subject: [PATCH 84/85] Test: improve after coderabbit suggestions integration tests for vet --- .../example/vet1177/security/CustomUserDetailsService.java | 4 ++-- .../org/example/vet1177/security/JwtAuthenticationFilter.java | 2 +- .../java/org/example/vet1177/security/SecurityConfig.java | 1 + .../org/example/vet1177/security/auth/dto/AuthRequest.java | 2 +- .../example/vet1177/security/auth/dto/RegisterRequest.java | 2 +- .../org/example/vet1177/controller/PetControllerTest.java | 2 +- .../org/example/vet1177/controller/UserControllerTest.java | 2 +- 7 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/example/vet1177/security/CustomUserDetailsService.java b/src/main/java/org/example/vet1177/security/CustomUserDetailsService.java index 3251b4ed..29443295 100644 --- a/src/main/java/org/example/vet1177/security/CustomUserDetailsService.java +++ b/src/main/java/org/example/vet1177/security/CustomUserDetailsService.java @@ -43,8 +43,8 @@ public UserDetails loadUserByUsername(String email) throws UsernameNotFoundExcep return userRepository.findByEmail(email) .orElseThrow(() -> { - log.warn("User not found email={}", email); - return new UsernameNotFoundException("Användare hittades inte: " + email); + log.warn("User not found during authentication"); + return new UsernameNotFoundException("Användare hittades inte"); }); } } diff --git a/src/main/java/org/example/vet1177/security/JwtAuthenticationFilter.java b/src/main/java/org/example/vet1177/security/JwtAuthenticationFilter.java index 519eb079..1885dc76 100644 --- a/src/main/java/org/example/vet1177/security/JwtAuthenticationFilter.java +++ b/src/main/java/org/example/vet1177/security/JwtAuthenticationFilter.java @@ -119,7 +119,7 @@ protected void doFilterInternal( } catch (UsernameNotFoundException e) { // Token är giltig men användaren finns inte längre i databasen // (t.ex. raderad av admin medan tokenen fortfarande gäller). - log.warn("User from JWT no longer exists: {}", e.getMessage()); + log.warn("User from JWT no longer exists in database"); } // Släpp vidare requesten till nästa filter i kedjan (och sedan till controllern) diff --git a/src/main/java/org/example/vet1177/security/SecurityConfig.java b/src/main/java/org/example/vet1177/security/SecurityConfig.java index 638cda8b..5bd2857a 100644 --- a/src/main/java/org/example/vet1177/security/SecurityConfig.java +++ b/src/main/java/org/example/vet1177/security/SecurityConfig.java @@ -147,6 +147,7 @@ public CorsConfigurationSource corsConfigurationSource() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/api/**", config); + source.registerCorsConfiguration("/pets/**", config); return source; } } diff --git a/src/main/java/org/example/vet1177/security/auth/dto/AuthRequest.java b/src/main/java/org/example/vet1177/security/auth/dto/AuthRequest.java index e9e4765b..cdc6354d 100644 --- a/src/main/java/org/example/vet1177/security/auth/dto/AuthRequest.java +++ b/src/main/java/org/example/vet1177/security/auth/dto/AuthRequest.java @@ -11,6 +11,6 @@ public record AuthRequest( ) { @Override public String toString() { - return "AuthRequest[email=" + email + ", password=***]"; + return "AuthRequest[email=***, password=***]"; } } diff --git a/src/main/java/org/example/vet1177/security/auth/dto/RegisterRequest.java b/src/main/java/org/example/vet1177/security/auth/dto/RegisterRequest.java index 3bc92ca4..43473b15 100644 --- a/src/main/java/org/example/vet1177/security/auth/dto/RegisterRequest.java +++ b/src/main/java/org/example/vet1177/security/auth/dto/RegisterRequest.java @@ -12,6 +12,6 @@ public record RegisterRequest( ) { @Override public String toString() { - return "RegisterRequest[name=" + name + ", email=" + email + ", password=***]"; + return "RegisterRequest[name=***, email=***, password=***]"; } } diff --git a/src/test/java/org/example/vet1177/controller/PetControllerTest.java b/src/test/java/org/example/vet1177/controller/PetControllerTest.java index 32ae72b6..1e27be9e 100644 --- a/src/test/java/org/example/vet1177/controller/PetControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/PetControllerTest.java @@ -4,7 +4,7 @@ import org.example.vet1177.security.CustomUserDetailsService; import org.example.vet1177.security.JwtService; import org.springframework.test.context.ActiveProfiles; -import tools.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectMapper; import org.example.vet1177.exception.ResourceNotFoundException; import org.example.vet1177.dto.request.pet.PetRequest; import org.example.vet1177.entities.Pet; diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index c63cbb94..29fabcf2 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -3,7 +3,7 @@ import org.example.vet1177.dto.request.user.UserUpdateRequest; import org.example.vet1177.exception.BusinessRuleException; import org.springframework.http.MediaType; -import tools.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectMapper; import org.example.vet1177.dto.request.user.UserRequest; import org.example.vet1177.dto.response.user.UserResponse; From 8b0d4846149edfb5e31e3adce247d731d2e10c78 Mon Sep 17 00:00:00 2001 From: Tatjana Date: Tue, 14 Apr 2026 22:47:43 +0200 Subject: [PATCH 85/85] Test: changes and improve code after coderabbit suggestions integration tests for vet --- .../java/org/example/vet1177/controller/PetControllerTest.java | 2 +- .../java/org/example/vet1177/controller/UserControllerTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/example/vet1177/controller/PetControllerTest.java b/src/test/java/org/example/vet1177/controller/PetControllerTest.java index 1e27be9e..32ae72b6 100644 --- a/src/test/java/org/example/vet1177/controller/PetControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/PetControllerTest.java @@ -4,7 +4,7 @@ import org.example.vet1177.security.CustomUserDetailsService; import org.example.vet1177.security.JwtService; import org.springframework.test.context.ActiveProfiles; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; import org.example.vet1177.exception.ResourceNotFoundException; import org.example.vet1177.dto.request.pet.PetRequest; import org.example.vet1177.entities.Pet; diff --git a/src/test/java/org/example/vet1177/controller/UserControllerTest.java b/src/test/java/org/example/vet1177/controller/UserControllerTest.java index 29fabcf2..c63cbb94 100644 --- a/src/test/java/org/example/vet1177/controller/UserControllerTest.java +++ b/src/test/java/org/example/vet1177/controller/UserControllerTest.java @@ -3,7 +3,7 @@ import org.example.vet1177.dto.request.user.UserUpdateRequest; import org.example.vet1177.exception.BusinessRuleException; import org.springframework.http.MediaType; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; import org.example.vet1177.dto.request.user.UserRequest; import org.example.vet1177.dto.response.user.UserResponse;