diff --git a/backend/backend-test-results.txt b/backend/backend-test-results.txt new file mode 100644 index 00000000..4f61b2fb --- /dev/null +++ b/backend/backend-test-results.txt @@ -0,0 +1,145 @@ +WARNING: A terminally deprecated method in sun.misc.Unsafe has been called +WARNING: sun.misc.Unsafe::staticFieldBase has been called by com.google.inject.internal.aop.HiddenClassDefiner (file:/nix/store/snv87hz5j78nqiqqamlf1mimbkmcrl6l-maven-3.9.11/maven/lib/guice-5.1.0-classes.jar) +WARNING: Please consider reporting this to the maintainers of class com.google.inject.internal.aop.HiddenClassDefiner +WARNING: sun.misc.Unsafe::staticFieldBase will be removed in a future release +[INFO] Scanning for projects... +[INFO] +[INFO] ------------------------< com.petshop:backend >------------------------- +[INFO] Building PetShop Backend 1.0.0 +[INFO] from pom.xml +[INFO] --------------------------------[ jar ]--------------------------------- +[INFO] +[INFO] --- enforcer:3.5.0:enforce (require-java-25) @ backend --- +[INFO] Rule 0: org.apache.maven.enforcer.rules.version.RequireJavaVersion passed +[INFO] +[INFO] --- resources:3.3.1:resources (default-resources) @ backend --- +[INFO] Copying 2 resources from src/main/resources to target/classes +[INFO] Copying 14 resources from src/main/resources to target/classes +[INFO] +[INFO] --- compiler:3.14.1:compile (default-compile) @ backend --- +[INFO] Nothing to compile - all classes are up to date. +[INFO] +[INFO] --- resources:3.3.1:testResources (default-testResources) @ backend --- +[INFO] skip non existing resourceDirectory /home/user/threaded-parity/backend/src/test/resources +[INFO] +[INFO] --- compiler:3.14.1:testCompile (default-testCompile) @ backend --- +[INFO] Nothing to compile - all classes are up to date. +[INFO] +[INFO] --- surefire:3.5.4:test (default-test) @ backend --- +[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider +[INFO] +[INFO] ------------------------------------------------------- +[INFO] T E S T S +[INFO] ------------------------------------------------------- +[INFO] Running com.petshop.backend.service.UserServiceTest +Mockito is currently self-attaching to enable the inline-mock-maker. This will no longer work in future releases of the JDK. Please add Mockito as an agent to your build as described in Mockito's documentation: https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html#0.3 +OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended +WARNING: A Java agent has been loaded dynamically (/home/user/.m2/repository/net/bytebuddy/byte-buddy-agent/1.17.8/byte-buddy-agent-1.17.8.jar) +WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning +WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information +WARNING: Dynamic loading of agents will be disallowed by default in a future release +[ERROR] Tests run: 10, Failures: 3, Errors: 2, Skipped: 0, Time elapsed: 0.624 s <<< FAILURE! -- in com.petshop.backend.service.UserServiceTest +[ERROR] com.petshop.backend.service.UserServiceTest.scopedUpdateDeniesRoleEscalation -- Time elapsed: 0.007 s <<< FAILURE! +org.opentest4j.AssertionFailedError: Unexpected exception type thrown, expected: but was: + at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:158) + at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:68) + at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:35) + at org.junit.jupiter.api.Assertions.assertThrows(Assertions.java:3223) + at com.petshop.backend.service.UserServiceTest.scopedUpdateDeniesRoleEscalation(UserServiceTest.java:181) +Caused by: com.petshop.backend.exception.ResourceNotFoundException: User not found with id: 2 + at com.petshop.backend.service.UserService.lambda$updateUser$0(UserService.java:113) + at java.base/java.util.Optional.orElseThrow(Optional.java:403) + at com.petshop.backend.service.UserService.updateUser(UserService.java:113) + at com.petshop.backend.service.UserServiceTest.lambda$scopedUpdateDeniesRoleEscalation$0(UserServiceTest.java:181) + at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:54) + ... 3 more + +[ERROR] com.petshop.backend.service.UserServiceTest.updateUserTreatsWrongScopedRoleAsNotFound -- Time elapsed: 0.005 s <<< ERROR! +org.mockito.exceptions.misusing.UnnecessaryStubbingException: + +Unnecessary stubbings detected. +Clean & maintainable test code requires zero unnecessary code. +Following stubbings are unnecessary (click to navigate to relevant line of code): + 1. -> at com.petshop.backend.service.UserServiceTest.updateUserTreatsWrongScopedRoleAsNotFound(UserServiceTest.java:75) +Please remove unnecessary stubbings or use 'lenient' strictness. More info: javadoc for UnnecessaryStubbingException class. + at org.mockito.junit.jupiter.MockitoExtension.lambda$afterEach$2(MockitoExtension.java:200) + at java.base/java.util.Optional.ifPresent(Optional.java:178) + at org.mockito.junit.jupiter.MockitoExtension.afterEach(MockitoExtension.java:198) + at java.base/java.util.ArrayList.forEach(ArrayList.java:1604) + at java.base/java.util.ArrayList.forEach(ArrayList.java:1604) + +[ERROR] com.petshop.backend.service.UserServiceTest.updateUserDeniesPromotingAnotherUserToAdmin -- Time elapsed: 0.004 s <<< FAILURE! +org.opentest4j.AssertionFailedError: Unexpected exception type thrown, expected: but was: + at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:158) + at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:68) + at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:35) + at org.junit.jupiter.api.Assertions.assertThrows(Assertions.java:3223) + at com.petshop.backend.service.UserServiceTest.updateUserDeniesPromotingAnotherUserToAdmin(UserServiceTest.java:167) +Caused by: com.petshop.backend.exception.ResourceNotFoundException: User not found with id: 2 + at com.petshop.backend.service.UserService.lambda$updateUser$0(UserService.java:113) + at java.base/java.util.Optional.orElseThrow(Optional.java:403) + at com.petshop.backend.service.UserService.updateUser(UserService.java:113) + at com.petshop.backend.service.UserService.updateUser(UserService.java:107) + at com.petshop.backend.service.UserServiceTest.lambda$updateUserDeniesPromotingAnotherUserToAdmin$0(UserServiceTest.java:167) + at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:54) + ... 3 more + +[ERROR] com.petshop.backend.service.UserServiceTest.updateUserAllowsEditingOwnAdminAccount -- Time elapsed: 0.003 s <<< ERROR! +com.petshop.backend.exception.ResourceNotFoundException: User not found with id: 1 + at com.petshop.backend.service.UserService.lambda$updateUser$0(UserService.java:113) + at java.base/java.util.Optional.orElseThrow(Optional.java:403) + at com.petshop.backend.service.UserService.updateUser(UserService.java:113) + at com.petshop.backend.service.UserService.updateUser(UserService.java:107) + at com.petshop.backend.service.UserServiceTest.updateUserAllowsEditingOwnAdminAccount(UserServiceTest.java:152) + +[ERROR] com.petshop.backend.service.UserServiceTest.updateUserDeniesEditingAnotherAdmin -- Time elapsed: 0.005 s <<< FAILURE! +org.opentest4j.AssertionFailedError: Unexpected exception type thrown, expected: but was: + at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:158) + at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:68) + at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:35) + at org.junit.jupiter.api.Assertions.assertThrows(Assertions.java:3223) + at com.petshop.backend.service.UserServiceTest.updateUserDeniesEditingAnotherAdmin(UserServiceTest.java:65) +Caused by: com.petshop.backend.exception.ResourceNotFoundException: User not found with id: 2 + at com.petshop.backend.service.UserService.lambda$updateUser$0(UserService.java:113) + at java.base/java.util.Optional.orElseThrow(Optional.java:403) + at com.petshop.backend.service.UserService.updateUser(UserService.java:113) + at com.petshop.backend.service.UserService.updateUser(UserService.java:107) + at com.petshop.backend.service.UserServiceTest.lambda$updateUserDeniesEditingAnotherAdmin$0(UserServiceTest.java:65) + at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:54) + ... 3 more + +[INFO] +[INFO] Results: +[INFO] +[ERROR] Failures: +[ERROR] UserServiceTest.scopedUpdateDeniesRoleEscalation:181 Unexpected exception type thrown, expected: but was: +[ERROR] UserServiceTest.updateUserDeniesEditingAnotherAdmin:65 Unexpected exception type thrown, expected: but was: +[ERROR] UserServiceTest.updateUserDeniesPromotingAnotherUserToAdmin:167 Unexpected exception type thrown, expected: but was: +[ERROR] Errors: +[ERROR] UserServiceTest.updateUserAllowsEditingOwnAdminAccount:152 » ResourceNotFound User not found with id: 1 +[ERROR] UserServiceTest.updateUserTreatsWrongScopedRoleAsNotFound » UnnecessaryStubbing +Unnecessary stubbings detected. +Clean & maintainable test code requires zero unnecessary code. +Following stubbings are unnecessary (click to navigate to relevant line of code): + 1. -> at com.petshop.backend.service.UserServiceTest.updateUserTreatsWrongScopedRoleAsNotFound(UserServiceTest.java:75) +Please remove unnecessary stubbings or use 'lenient' strictness. More info: javadoc for UnnecessaryStubbingException class. +[INFO] +[ERROR] Tests run: 10, Failures: 3, Errors: 2, Skipped: 0 +[INFO] +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD FAILURE +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 2.040 s +[INFO] Finished at: 2026-04-16T08:04:25-06:00 +[INFO] ------------------------------------------------------------------------ +[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.5.4:test (default-test) on project backend: There are test failures. +[ERROR] +[ERROR] See /home/user/threaded-parity/backend/target/surefire-reports for the individual test results. +[ERROR] See dump files (if any exist) [date].dump, [date]-jvmRun[N].dump and [date].dumpstream. +[ERROR] -> [Help 1] +[ERROR] +[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. +[ERROR] Re-run Maven using the -X switch to enable full debug logging. +[ERROR] +[ERROR] For more information about the errors and possible solutions, please read the following articles: +[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException diff --git a/backend/src/main/java/com/petshop/backend/controller/AiChatController.java b/backend/src/main/java/com/petshop/backend/controller/AiChatController.java index ba08b418..f65dbaf0 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AiChatController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AiChatController.java @@ -56,7 +56,8 @@ public class AiChatController { List userPets; try { - userPets = petRepository.findAllByOwner_IdOrderByPetNameAsc(user.getId()); + userPets = petRepository.findAllByOwner_IdAndPetStatusInOrderByPetNameAsc( + user.getId(), List.of("Adopted", "Owned")); } catch (Exception e) { diff --git a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java index e18a007a..59243df2 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java @@ -53,5 +53,8 @@ public interface AppointmentRepository extends JpaRepository List findByPet_Id(Long petId); + @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.pet.petId = :petId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") + List findByPetIdAndAppointmentDate(@Param("petId") Long petId, @Param("date") LocalDate date); + List findByAppointmentDateAndAppointmentStatusIgnoreCase(LocalDate date, String status); } diff --git a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java index d301be55..c5f8d73a 100644 --- a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java @@ -37,6 +37,7 @@ public interface PetRepository extends JpaRepository { List findAdoptablePetsByStore(@Param("storeId") Long storeId); List findAllByOwner_IdOrderByPetNameAsc(Long ownerId); + List findAllByOwner_IdAndPetStatusInOrderByPetNameAsc(Long ownerId, List statuses); Optional findByIdAndOwner_Id(Long id, Long ownerId); @Lock(LockModeType.PESSIMISTIC_WRITE) 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 9c63733a..315fe509 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -130,6 +130,7 @@ public class AppointmentService { validateStoreAccess(store.getStoreId(), authenticatedUser); validatePetServiceCompatibility(pet, service); validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), null); + validatePetAvailability(pet, service, request.getAppointmentDate(), request.getAppointmentTime(), null); Appointment appointment = new Appointment(); appointment.setCustomer(customer); @@ -170,6 +171,7 @@ public class AppointmentService { validateStoreAccess(store.getStoreId(), authenticatedUser); validatePetServiceCompatibility(pet, service); validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), id); + validatePetAvailability(pet, service, request.getAppointmentDate(), request.getAppointmentTime(), id); appointment.setCustomer(customer); appointment.setStore(store); @@ -385,6 +387,15 @@ public class AppointmentService { return true; } + private void validatePetAvailability(Pet pet, com.petshop.backend.entity.Service service, LocalDate date, LocalTime time, Long appointmentIdToIgnore) { + if (pet == null) return; + List existingAppointments = appointmentRepository + .findByPetIdAndAppointmentDate(pet.getPetId(), date); + if (!isSlotAvailable(existingAppointments, service, time, appointmentIdToIgnore)) { + throw new IllegalArgumentException("This pet already has an appointment during this time slot"); + } + } + private void validateStoreAccess(Long requestedStoreId, User user) { if (user.getRole() != User.Role.STAFF) { return; diff --git a/web/app/ai-chat/page.js b/web/app/ai-chat/page.js index 2f35f51a..13ed9ae4 100644 --- a/web/app/ai-chat/page.js +++ b/web/app/ai-chat/page.js @@ -111,16 +111,18 @@ function AiChatPage() { lastScrolledIdRef.current = lastMsg.id; const area = messagesAreaRef.current; if (!area) return; - const nearBottom = area.scrollHeight - area.scrollTop - area.clientHeight < 150; - if (nearBottom) messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + const nearBottom = area.scrollHeight - area.scrollTop - area.clientHeight < 80; + if (nearBottom) { + area.scrollTop = area.scrollHeight; + } }, [messages]); useEffect(() => { if (!botTyping) return; const area = messagesAreaRef.current; if (!area) return; - const nearBottom = area.scrollHeight - area.scrollTop - area.clientHeight < 150; - if (nearBottom) messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + const nearBottom = area.scrollHeight - area.scrollTop - area.clientHeight < 80; + if (nearBottom) area.scrollTop = area.scrollHeight; }, [botTyping]); const fetchMessages = useCallback(async (convId) => { @@ -214,6 +216,8 @@ function AiChatPage() { useEffect(() => { if (!token || authLoading) return; + let stale = false; + async function init() { setLoadingConv(true); setError(null); @@ -225,6 +229,7 @@ function AiChatPage() { const res = await fetch(`${API_BASE}/api/v1/chat/conversations`, { headers: { Authorization: `Bearer ${token}` }, }); + if (stale) return; if (res.ok) { const list = await res.json(); const openAi = Array.isArray(list) @@ -233,12 +238,12 @@ function AiChatPage() { if (openAi) convId = openAi.id; } } catch { + if (stale) return; setError("Failed to load conversations."); } } if (!convId) { - // Auto-create a new AI conversation try { const res = await fetch(`${API_BASE}/api/v1/chat/conversations`, { method: "POST", @@ -248,17 +253,19 @@ function AiChatPage() { }, body: JSON.stringify({}), }); + if (stale) return; if (res.ok) { const conv = await res.json(); convId = conv.id; } } catch { - // silent + if (stale) return; } } if (!convId) { await fetchConversations(); + if (stale) return; setLoadingConv(false); return; } @@ -268,6 +275,7 @@ function AiChatPage() { fetchMessages(convId), fetchConversations(), ]); + if (stale) return; setLoadingConv(false); connectStomp(convId); router.replace(`/ai-chat?id=${convId}`, { scroll: false }); @@ -276,6 +284,7 @@ function AiChatPage() { init(); return () => { + stale = true; if (stompRef.current) { stompRef.current.deactivate(); stompRef.current = null; } }; }, [token, authLoading, conversationIdParam, fetchConversation, fetchMessages, connectStomp, fetchConversations, router]); @@ -431,11 +440,6 @@ function AiChatPage() { setMessages([]); setError(null); setBotTyping(false); - setSwitchingConv(true); - await fetchConversation(convId); - await fetchMessages(convId); - setSwitchingConv(false); - connectStomp(convId); router.replace(`/ai-chat?id=${convId}`, { scroll: false }); } diff --git a/web/app/appointments/page.js b/web/app/appointments/page.js index 1492909b..89bce924 100644 --- a/web/app/appointments/page.js +++ b/web/app/appointments/page.js @@ -609,7 +609,7 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN"; }, [storeId, serviceId, appointmentDate]); const eligiblePets = customerPets.filter( - (p) => p.petStatus === "Owned" || p.petStatus === "Adopted" + (p) => p.petStatus?.toLowerCase() === "owned" || p.petStatus?.toLowerCase() === "adopted" ); const selectedService = services.find((s) => s.serviceId === Number(serviceId)); @@ -669,7 +669,7 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN"; return; } - if (!adoptionMode && selectedPet && selectedPet.petStatus !== "Owned" && selectedPet.petStatus !== "Adopted") { + if (!adoptionMode && selectedPet && selectedPet.petStatus?.toLowerCase() !== "owned" && selectedPet.petStatus?.toLowerCase() !== "adopted") { setError("The selected pet is no longer eligible for appointments. Please refresh the page."); return; } diff --git a/web/app/chat/page.js b/web/app/chat/page.js index 8f42cb35..17d7c42c 100644 --- a/web/app/chat/page.js +++ b/web/app/chat/page.js @@ -215,6 +215,8 @@ function ChatPage() { useEffect(() => { if (!token || authLoading) return; + let stale = false; + async function init() { setLoadingConv(true); setError(null); @@ -226,6 +228,7 @@ function ChatPage() { const res = await fetch(`${API_BASE}/api/v1/chat/conversations`, { headers: { Authorization: `Bearer ${token}` }, }); + if (stale) return; if (res.ok) { const list = await res.json(); const open = Array.isArray(list) @@ -234,12 +237,14 @@ function ChatPage() { if (open) convId = open.id; } } catch { + if (stale) return; setError("Failed to load conversations."); } } if (!convId) { await fetchConversations(); + if (stale) return; setLoadingConv(false); setConversation(null); return; @@ -250,6 +255,7 @@ function ChatPage() { fetchMessages(convId), fetchConversations(), ]); + if (stale) return; setLoadingConv(false); connectStomp(convId); } @@ -257,6 +263,7 @@ function ChatPage() { init(); return () => { + stale = true; if (stompRef.current) { stompRef.current.deactivate(); stompRef.current = null; } }; }, [token, authLoading, conversationIdParam, fetchConversation, fetchMessages, connectStomp, fetchConversations]); @@ -415,11 +422,6 @@ function ChatPage() { if (stompRef.current) { stompRef.current.deactivate(); stompRef.current = null; } setMessages([]); setError(null); - setSwitchingConv(true); - await fetchConversation(convId); - await fetchMessages(convId); - setSwitchingConv(false); - connectStomp(convId); router.replace(`/chat?id=${convId}`, { scroll: false }); } diff --git a/web/app/globals.css b/web/app/globals.css index dde87b4b..bcb053a8 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -58,6 +58,8 @@ body { align-items: center; gap: 1.25rem; justify-content: center; + min-width: 0; + overflow: hidden; } /* Indivdual Link Styles */