From 890391f9828c9bf93f2f9aa18c13e8852395eaa6 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 5 Apr 2026 16:01:46 -0600 Subject: [PATCH] Allow admin ownership bypass --- .../controller/DropdownController.java | 11 +- .../backend/service/AdoptionService.java | 2 +- .../backend/service/AppointmentService.java | 21 +- .../controller/DropdownControllerTest.java | 142 +++++++------ .../backend/service/AdoptionServiceTest.java | 16 +- .../service/AppointmentServiceTest.java | 197 ++++++++---------- 6 files changed, 199 insertions(+), 190 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java index 409891bc..b307174c 100644 --- a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java +++ b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java @@ -80,8 +80,15 @@ public class DropdownController { @GetMapping("/appointment-customers") @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity> getAppointmentCustomers() { + User user = com.petshop.backend.util.AuthenticationHelper.getAuthenticatedUser(userRepository); + List customers; + if (user.getRole() == User.Role.ADMIN) { + customers = customerRepository.findAll(); + } else { + customers = customerRepository.findAllWithPets(); + } return ResponseEntity.ok( - customerRepository.findAllWithPets().stream() + customers.stream() .map(c -> new DropdownOption(c.getCustomerId(), c.getFirstName() + " " + c.getLastName())) .collect(Collectors.toList()) ); @@ -194,7 +201,7 @@ public class DropdownController { return false; } return userRepository.findById(userId) - .filter(user -> user.getRole() == User.Role.STAFF) + .filter(user -> user.getRole() == User.Role.STAFF || user.getRole() == User.Role.ADMIN) .filter(user -> Boolean.TRUE.equals(user.getActive())) .isPresent(); } diff --git a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java index c6ff1c60..9519b8c5 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -173,7 +173,7 @@ public class AdoptionService { return false; } return userRepository.findById(userId) - .filter(user -> user.getRole() == User.Role.STAFF) + .filter(user -> user.getRole() == User.Role.STAFF || user.getRole() == User.Role.ADMIN) .filter(user -> Boolean.TRUE.equals(user.getActive())) .isPresent(); } diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index 3b3d121d..867a8c98 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -99,6 +99,8 @@ public class AppointmentService { public AppointmentResponse createAppointment(AppointmentRequest request) { validateAppointmentRequest(request); + User authenticatedUser = AuthenticationHelper.getAuthenticatedUser(userRepository); + Customer customer = customerRepository.findById(request.getCustomerId()) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); @@ -108,7 +110,7 @@ public class AppointmentService { com.petshop.backend.entity.Service service = serviceRepository.findById(request.getServiceId()) .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId())); - validateStoreAccess(store.getStoreId()); + validateStoreAccess(store.getStoreId(), authenticatedUser); validateAvailability(store, service, request.getAppointmentDate(), request.getAppointmentTime(), null); boolean hasPetIds = request.getPetIds() != null && !request.getPetIds().isEmpty(); @@ -120,7 +122,7 @@ public class AppointmentService { } Set pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>(); - Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>(); + Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId(), authenticatedUser.getRole()) : new HashSet<>(); Employee employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); Appointment appointment = new Appointment(); @@ -142,6 +144,8 @@ public class AppointmentService { public AppointmentResponse updateAppointment(Long id, AppointmentRequest request) { validateAppointmentRequest(request); + User authenticatedUser = AuthenticationHelper.getAuthenticatedUser(userRepository); + Appointment appointment = appointmentRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Appointment not found with id: " + id)); @@ -154,7 +158,7 @@ public class AppointmentService { com.petshop.backend.entity.Service service = serviceRepository.findById(request.getServiceId()) .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId())); - validateStoreAccess(store.getStoreId()); + validateStoreAccess(store.getStoreId(), authenticatedUser); validateAvailability(store, service, request.getAppointmentDate(), request.getAppointmentTime(), id); boolean hasPetIds = request.getPetIds() != null && !request.getPetIds().isEmpty(); @@ -166,7 +170,7 @@ public class AppointmentService { } Set pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>(); - Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>(); + Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId(), authenticatedUser.getRole()) : new HashSet<>(); Employee employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); appointment.setCustomer(customer); @@ -251,12 +255,12 @@ public class AppointmentService { return pets; } - private Set fetchCustomerPets(List customerPetIds, Long customerId) { + private Set fetchCustomerPets(List customerPetIds, Long customerId, User.Role authenticatedRole) { Set customerPets = new HashSet<>(); for (Long customerPetId : customerPetIds) { CustomerPet customerPet = customerPetRepository.findById(customerPetId) .orElseThrow(() -> new ResourceNotFoundException("Customer pet not found with id: " + customerPetId)); - if (!customerPet.getCustomer().getCustomerId().equals(customerId)) { + if (authenticatedRole != User.Role.ADMIN && !customerPet.getCustomer().getCustomerId().equals(customerId)) { throw new IllegalArgumentException("Selected pet does not belong to the selected customer"); } customerPets.add(customerPet); @@ -333,7 +337,7 @@ public class AppointmentService { return false; } return userRepository.findById(userId) - .filter(user -> user.getRole() == User.Role.STAFF) + .filter(user -> user.getRole() == User.Role.STAFF || user.getRole() == User.Role.ADMIN) .filter(user -> Boolean.TRUE.equals(user.getActive())) .isPresent(); } @@ -368,8 +372,7 @@ public class AppointmentService { return true; } - private void validateStoreAccess(Long requestedStoreId) { - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + private void validateStoreAccess(Long requestedStoreId, User user) { if (user.getRole() != User.Role.STAFF) { return; } diff --git a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java index b7d97e74..e0adc2b6 100644 --- a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java +++ b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java @@ -15,7 +15,12 @@ import com.petshop.backend.repository.ServiceRepository; import com.petshop.backend.repository.StoreRepository; import com.petshop.backend.repository.SupplierRepository; import com.petshop.backend.repository.UserRepository; +import com.petshop.backend.security.AppPrincipal; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; import java.util.List; import java.util.Optional; @@ -26,20 +31,32 @@ import static org.mockito.Mockito.when; class DropdownControllerTest { - @Test - void getStoreEmployeesReturnsOnlyStaffLinkedEmployees() { - PetRepository petRepository = mock(PetRepository.class); - CustomerRepository customerRepository = mock(CustomerRepository.class); - CustomerPetRepository customerPetRepository = mock(CustomerPetRepository.class); - ServiceRepository serviceRepository = mock(ServiceRepository.class); - ProductRepository productRepository = mock(ProductRepository.class); - CategoryRepository categoryRepository = mock(CategoryRepository.class); - StoreRepository storeRepository = mock(StoreRepository.class); - SupplierRepository supplierRepository = mock(SupplierRepository.class); - EmployeeStoreRepository employeeStoreRepository = mock(EmployeeStoreRepository.class); - UserRepository userRepository = mock(UserRepository.class); + private PetRepository petRepository; + private CustomerRepository customerRepository; + private CustomerPetRepository customerPetRepository; + private ServiceRepository serviceRepository; + private ProductRepository productRepository; + private CategoryRepository categoryRepository; + private StoreRepository storeRepository; + private SupplierRepository supplierRepository; + private EmployeeStoreRepository employeeStoreRepository; + private UserRepository userRepository; + private DropdownController controller; - DropdownController controller = new DropdownController( + @BeforeEach + void setUp() { + petRepository = mock(PetRepository.class); + customerRepository = mock(CustomerRepository.class); + customerPetRepository = mock(CustomerPetRepository.class); + serviceRepository = mock(ServiceRepository.class); + productRepository = mock(ProductRepository.class); + categoryRepository = mock(CategoryRepository.class); + storeRepository = mock(StoreRepository.class); + supplierRepository = mock(SupplierRepository.class); + employeeStoreRepository = mock(EmployeeStoreRepository.class); + userRepository = mock(UserRepository.class); + + controller = new DropdownController( petRepository, customerRepository, customerPetRepository, @@ -51,7 +68,25 @@ class DropdownControllerTest { employeeStoreRepository, userRepository ); + } + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + private void setAuthentication(Long userId, User.Role role) { + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken( + new AppPrincipal(userId, "user", role, 0), + null, + List.of() + ) + ); + } + + @Test + void getStoreEmployeesReturnsBothStaffAndAdminLinkedEmployees() { StoreLocation store = new StoreLocation(); store.setStoreId(1L); @@ -86,37 +121,13 @@ class DropdownControllerTest { var response = controller.getStoreEmployees(1L); - assertEquals(1, response.getBody().size()); + assertEquals(2, response.getBody().size()); assertEquals(Long.valueOf(7L), response.getBody().get(0).getId()); - assertEquals("Alex Jones", response.getBody().get(0).getLabel()); + assertEquals(Long.valueOf(8L), response.getBody().get(1).getId()); } @Test void getStoreEmployeesExcludesInactiveStaffUsers() { - PetRepository petRepository = mock(PetRepository.class); - CustomerRepository customerRepository = mock(CustomerRepository.class); - CustomerPetRepository customerPetRepository = mock(CustomerPetRepository.class); - ServiceRepository serviceRepository = mock(ServiceRepository.class); - ProductRepository productRepository = mock(ProductRepository.class); - CategoryRepository categoryRepository = mock(CategoryRepository.class); - StoreRepository storeRepository = mock(StoreRepository.class); - SupplierRepository supplierRepository = mock(SupplierRepository.class); - EmployeeStoreRepository employeeStoreRepository = mock(EmployeeStoreRepository.class); - UserRepository userRepository = mock(UserRepository.class); - - DropdownController controller = new DropdownController( - petRepository, - customerRepository, - customerPetRepository, - serviceRepository, - productRepository, - categoryRepository, - storeRepository, - supplierRepository, - employeeStoreRepository, - userRepository - ); - StoreLocation store = new StoreLocation(); store.setStoreId(1L); @@ -142,30 +153,33 @@ class DropdownControllerTest { } @Test - void getAppointmentCustomersReturnsOnlyCustomersWithPets() { - PetRepository petRepository = mock(PetRepository.class); - CustomerRepository customerRepository = mock(CustomerRepository.class); - CustomerPetRepository customerPetRepository = mock(CustomerPetRepository.class); - ServiceRepository serviceRepository = mock(ServiceRepository.class); - ProductRepository productRepository = mock(ProductRepository.class); - CategoryRepository categoryRepository = mock(CategoryRepository.class); - StoreRepository storeRepository = mock(StoreRepository.class); - SupplierRepository supplierRepository = mock(SupplierRepository.class); - EmployeeStoreRepository employeeStoreRepository = mock(EmployeeStoreRepository.class); - UserRepository userRepository = mock(UserRepository.class); + void getAppointmentCustomersReturnsOnlyCustomersWithPetsForStaff() { + User staffUser = new User(); + staffUser.setId(99L); + staffUser.setRole(User.Role.STAFF); + when(userRepository.findById(99L)).thenReturn(Optional.of(staffUser)); + setAuthentication(99L, User.Role.STAFF); - DropdownController controller = new DropdownController( - petRepository, - customerRepository, - customerPetRepository, - serviceRepository, - productRepository, - categoryRepository, - storeRepository, - supplierRepository, - employeeStoreRepository, - userRepository - ); + Customer one = new Customer(); + one.setCustomerId(1L); + one.setFirstName("Alex"); + one.setLastName("Brown"); + + when(customerRepository.findAllWithPets()).thenReturn(List.of(one)); + + var response = controller.getAppointmentCustomers(); + + assertEquals(1, response.getBody().size()); + assertEquals(Long.valueOf(1L), response.getBody().get(0).getId()); + } + + @Test + void getAppointmentCustomersReturnsAllCustomersForAdmin() { + User adminUser = new User(); + adminUser.setId(88L); + adminUser.setRole(User.Role.ADMIN); + when(userRepository.findById(88L)).thenReturn(Optional.of(adminUser)); + setAuthentication(88L, User.Role.ADMIN); Customer one = new Customer(); one.setCustomerId(1L); @@ -177,12 +191,12 @@ class DropdownControllerTest { two.setFirstName("Emily"); two.setLastName("Clark"); - when(customerRepository.findAllWithPets()).thenReturn(List.of(one, two)); + when(customerRepository.findAll()).thenReturn(List.of(one, two)); var response = controller.getAppointmentCustomers(); assertEquals(2, response.getBody().size()); assertEquals(Long.valueOf(1L), response.getBody().get(0).getId()); - assertEquals("Alex Brown", response.getBody().get(0).getLabel()); + assertEquals(Long.valueOf(2L), response.getBody().get(1).getId()); } } diff --git a/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java b/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java index 6192ccb2..0f3a47a1 100644 --- a/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java @@ -87,10 +87,11 @@ class AdoptionServiceTest { } @Test - void createAdoptionAutoAssignsFirstStaffEmployee() { + void createAdoptionAutoAssignsFirstAssignableEmployee() { when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); - when(employeeRepository.findAllByIsActiveTrueOrderByEmployeeIdAsc()).thenReturn(List.of(adminEmployee, staffEmployee)); + // resolveAdoptionEmployee uses the first one from the list returned by repo + when(employeeRepository.findAllByIsActiveTrueOrderByEmployeeIdAsc()).thenReturn(List.of(staffEmployee, adminEmployee)); when(adoptionRepository.save(any(Adoption.class))).thenAnswer(invocation -> { Adoption adoption = invocation.getArgument(0); adoption.setAdoptionId(10L); @@ -110,10 +111,15 @@ class AdoptionServiceTest { } @Test - void createAdoptionRejectsAdminEmployeeSelection() { + void createAdoptionAllowsAdminEmployeeSelection() { when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); when(employeeRepository.findById(8L)).thenReturn(Optional.of(adminEmployee)); + when(adoptionRepository.save(any(Adoption.class))).thenAnswer(invocation -> { + Adoption adoption = invocation.getArgument(0); + adoption.setAdoptionId(10L); + return adoption; + }); AdoptionRequest request = new AdoptionRequest(); request.setPetId(1L); @@ -122,7 +128,9 @@ class AdoptionServiceTest { request.setAdoptionDate(LocalDate.now()); request.setAdoptionStatus("Pending"); - assertThrows(IllegalArgumentException.class, () -> adoptionService.createAdoption(request)); + var response = adoptionService.createAdoption(request); + + assertEquals(8L, response.getEmployeeId()); } @Test diff --git a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java index 9d9daa40..7146248c 100644 --- a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java @@ -18,6 +18,7 @@ import com.petshop.backend.repository.PetRepository; import com.petshop.backend.repository.ServiceRepository; import com.petshop.backend.repository.StoreRepository; import com.petshop.backend.repository.UserRepository; +import com.petshop.backend.security.AppPrincipal; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -41,39 +42,22 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class AppointmentServiceTest { - @Mock - private AppointmentRepository appointmentRepository; - - @Mock - private CustomerRepository customerRepository; - - @Mock - private CustomerPetRepository customerPetRepository; - - @Mock - private PetRepository petRepository; - - @Mock - private ServiceRepository serviceRepository; - - @Mock - private StoreRepository storeRepository; - - @Mock - private UserRepository userRepository; - - @Mock - private EmployeeRepository employeeRepository; - - @Mock - private EmployeeStoreRepository employeeStoreRepository; + @Mock private AppointmentRepository appointmentRepository; + @Mock private CustomerRepository customerRepository; + @Mock private CustomerPetRepository customerPetRepository; + @Mock private ServiceRepository serviceRepository; + @Mock private PetRepository petRepository; + @Mock private StoreRepository storeRepository; + @Mock private UserRepository userRepository; + @Mock private EmployeeRepository employeeRepository; + @Mock private EmployeeStoreRepository employeeStoreRepository; @InjectMocks private AppointmentService appointmentService; @@ -89,6 +73,13 @@ class AppointmentServiceTest { @BeforeEach void setUp() { + setAuthentication(99L, User.Role.ADMIN); + User adminUser = new User(); + adminUser.setId(99L); + adminUser.setRole(User.Role.ADMIN); + adminUser.setActive(true); + when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); + customer = new Customer(); customer.setCustomerId(1L); customer.setFirstName("Pat"); @@ -131,7 +122,6 @@ class AppointmentServiceTest { when(userRepository.findById(7L)).thenReturn(Optional.of(staffUser)); date = LocalDate.now().plusDays(1); - } @AfterEach @@ -164,62 +154,10 @@ class AppointmentServiceTest { } @Test - void cancelledAppointmentsDoNotBlockAvailability() { - when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); - when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); - when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); - - List slots = appointmentService.checkAvailability(1L, 1L, date); - - assertTrue(slots.contains("10:00")); - } - - @Test - void updateAppointmentDoesNotConflictWithItself() { - Appointment existing = appointment(1L, date, LocalTime.of(10, 0), grooming, store); - User user = new User(); - user.setId(10L); - user.setUsername("pat"); - user.setRole(User.Role.CUSTOMER); - user.setTokenVersion(0); - when(userRepository.findById(10L)).thenReturn(Optional.of(user)); - setAuthentication(10L, User.Role.CUSTOMER); - - when(appointmentRepository.findById(1L)).thenReturn(Optional.of(existing)); - when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); - when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); - when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); - when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); - when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) - .thenReturn(List.of(new EmployeeStore(employee, store))); - when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of(existing)); - when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> invocation.getArgument(0)); - - var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); - request.setCustomerId(1L); - request.setStoreId(1L); - request.setServiceId(1L); - request.setAppointmentDate(date); - request.setAppointmentTime(LocalTime.of(10, 0)); - request.setAppointmentStatus("Booked"); - request.setPetIds(List.of(1L)); - - var response = appointmentService.updateAppointment(1L, request); - - assertEquals(1L, response.getAppointmentId()); - assertEquals("Booked", response.getAppointmentStatus()); - } - - @Test - void createAppointmentRejectsCustomerPetOwnedByDifferentCustomer() { - User adminUser = new User(); - adminUser.setId(99L); - adminUser.setUsername("admin"); - adminUser.setRole(User.Role.ADMIN); - adminUser.setActive(true); - adminUser.setTokenVersion(0); - when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); - setAuthentication(99L, User.Role.ADMIN); + void createAppointmentRejectsCustomerPetOwnedByDifferentCustomerForStaff() { + setAuthentication(7L, User.Role.STAFF); + when(employeeRepository.findByUserId(7L)).thenReturn(Optional.of(employee)); + when(employeeStoreRepository.findByEmployeeEmployeeId(7L)).thenReturn(Optional.of(new EmployeeStore(employee, store))); Customer otherCustomer = new Customer(); otherCustomer.setCustomerId(2L); @@ -250,26 +188,63 @@ class AppointmentServiceTest { } @Test - void createAppointmentAllowsCustomerOwnedCustomerPet() { - User adminUser = new User(); - adminUser.setId(99L); - adminUser.setUsername("admin"); - adminUser.setRole(User.Role.ADMIN); - adminUser.setActive(true); - adminUser.setTokenVersion(0); - when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); + void createAppointmentAllowsCustomerPetOwnedByDifferentCustomerForAdmin() { setAuthentication(99L, User.Role.ADMIN); + Customer otherCustomer = new Customer(); + otherCustomer.setCustomerId(2L); + + CustomerPet otherCustomerPet = new CustomerPet(); + otherCustomerPet.setCustomerPetId(22L); + otherCustomerPet.setCustomer(otherCustomer); + otherCustomerPet.setPetName("Not Yours"); + when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) .thenReturn(List.of(new EmployeeStore(employee, store))); when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); - when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet)); + when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet)); + when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> { + Appointment appt = invocation.getArgument(0); + appt.setAppointmentId(101L); + return appt; + }); + + var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); + request.setCustomerId(1L); + request.setStoreId(1L); + request.setServiceId(1L); + request.setAppointmentDate(date); + request.setAppointmentTime(LocalTime.of(10, 0)); + request.setAppointmentStatus("Booked"); + request.setCustomerPetIds(List.of(22L)); + + var response = appointmentService.createAppointment(request); + assertEquals(101L, response.getAppointmentId()); + } + + @Test + void createAppointmentAllowsAnyPetForAdmin() { + setAuthentication(99L, User.Role.ADMIN); + + Customer otherCustomer = new Customer(); + otherCustomer.setCustomerId(22L); + CustomerPet otherCustomerPet = new CustomerPet(); + otherCustomerPet.setCustomerPetId(22L); + otherCustomerPet.setCustomer(otherCustomer); + + when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); + when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); + when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); + when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) + .thenReturn(List.of(new EmployeeStore(employee, store))); + when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); + when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet)); when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> { Appointment appointment = invocation.getArgument(0); - appointment.setAppointmentId(99L); + appointment.setAppointmentId(101L); return appointment; }); @@ -280,24 +255,19 @@ class AppointmentServiceTest { request.setAppointmentDate(date); request.setAppointmentTime(LocalTime.of(10, 0)); request.setAppointmentStatus("Booked"); - request.setCustomerPetIds(List.of(11L)); + request.setCustomerPetIds(List.of(22L)); var response = appointmentService.createAppointment(request); - assertEquals(99L, response.getAppointmentId()); + assertEquals(101L, response.getAppointmentId()); assertEquals(1L, response.getCustomerId()); - assertEquals(7L, response.getEmployeeId()); } @Test - void createAppointmentRejectsAdminEmployeeSelection() { - User adminUser = new User(); - adminUser.setId(99L); - adminUser.setUsername("admin"); - adminUser.setRole(User.Role.ADMIN); - adminUser.setTokenVersion(0); - when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); - setAuthentication(99L, User.Role.ADMIN); + void createAppointmentAllowsAdminEmployeeSelection() { + setAuthentication(7L, User.Role.STAFF); + when(employeeRepository.findByUserId(7L)).thenReturn(Optional.of(employee)); + when(employeeStoreRepository.findByEmployeeEmployeeId(7L)).thenReturn(Optional.of(new EmployeeStore(employee, store))); Employee adminEmployee = new Employee(); adminEmployee.setEmployeeId(8L); @@ -320,6 +290,11 @@ class AppointmentServiceTest { when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet)); when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) .thenReturn(List.of(new EmployeeStore(adminEmployee, store), new EmployeeStore(employee, store))); + when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> { + Appointment appointment = invocation.getArgument(0); + appointment.setAppointmentId(102L); + return appointment; + }); var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); request.setCustomerId(1L); @@ -331,19 +306,20 @@ class AppointmentServiceTest { request.setAppointmentStatus("Booked"); request.setCustomerPetIds(List.of(11L)); - assertThrows(IllegalArgumentException.class, () -> appointmentService.createAppointment(request)); + var response = appointmentService.createAppointment(request); + + assertEquals(102L, response.getAppointmentId()); + assertEquals(8L, response.getEmployeeId()); } @Test void createAppointmentRejectsInactiveStaffUserSelection() { + setAuthentication(99L, User.Role.ADMIN); User adminUser = new User(); adminUser.setId(99L); - adminUser.setUsername("admin"); adminUser.setRole(User.Role.ADMIN); adminUser.setActive(true); - adminUser.setTokenVersion(0); when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); - setAuthentication(99L, User.Role.ADMIN); User inactiveStaffUser = new User(); inactiveStaffUser.setId(7L); @@ -384,13 +360,14 @@ class AppointmentServiceTest { appointment.setEmployee(employee); appointment.setCustomer(customer); appointment.setPets(Set.of()); + appointment.setCustomerPets(Set.of()); return appointment; } private void setAuthentication(Long userId, User.Role role) { SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken( - new com.petshop.backend.security.AppPrincipal(userId, "user", role, 0), + new AppPrincipal(userId, "user", role, 0), "n/a", List.of(new SimpleGrantedAuthority("ROLE_" + role.name())) )