Merge pull request #322 from RecentRunner/android-desktop-parity

fix six app bugs
This commit is contained in:
2026-04-16 08:13:06 -06:00
committed by GitHub
9 changed files with 188 additions and 19 deletions

View File

@@ -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: <org.springframework.security.access.AccessDeniedException> but was: <com.petshop.backend.exception.ResourceNotFoundException>
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: <org.springframework.security.access.AccessDeniedException> but was: <com.petshop.backend.exception.ResourceNotFoundException>
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: <org.springframework.security.access.AccessDeniedException> but was: <com.petshop.backend.exception.ResourceNotFoundException>
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: <org.springframework.security.access.AccessDeniedException> but was: <com.petshop.backend.exception.ResourceNotFoundException>
[ERROR] UserServiceTest.updateUserDeniesEditingAnotherAdmin:65 Unexpected exception type thrown, expected: <org.springframework.security.access.AccessDeniedException> but was: <com.petshop.backend.exception.ResourceNotFoundException>
[ERROR] UserServiceTest.updateUserDeniesPromotingAnotherUserToAdmin:167 Unexpected exception type thrown, expected: <org.springframework.security.access.AccessDeniedException> but was: <com.petshop.backend.exception.ResourceNotFoundException>
[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

View File

@@ -56,7 +56,8 @@ public class AiChatController {
List<Pet> userPets;
try {
userPets = petRepository.findAllByOwner_IdOrderByPetNameAsc(user.getId());
userPets = petRepository.findAllByOwner_IdAndPetStatusInOrderByPetNameAsc(
user.getId(), List.of("Adopted", "Owned"));
}
catch (Exception e) {

View File

@@ -53,5 +53,8 @@ public interface AppointmentRepository extends JpaRepository<Appointment, Long>
List<Appointment> 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<Appointment> findByPetIdAndAppointmentDate(@Param("petId") Long petId, @Param("date") LocalDate date);
List<Appointment> findByAppointmentDateAndAppointmentStatusIgnoreCase(LocalDate date, String status);
}

View File

@@ -37,6 +37,7 @@ public interface PetRepository extends JpaRepository<Pet, Long> {
List<Pet> findAdoptablePetsByStore(@Param("storeId") Long storeId);
List<Pet> findAllByOwner_IdOrderByPetNameAsc(Long ownerId);
List<Pet> findAllByOwner_IdAndPetStatusInOrderByPetNameAsc(Long ownerId, List<String> statuses);
Optional<Pet> findByIdAndOwner_Id(Long id, Long ownerId);
@Lock(LockModeType.PESSIMISTIC_WRITE)

View File

@@ -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<Appointment> 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;

View File

@@ -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 });
}

View File

@@ -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;
}

View File

@@ -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 });
}

View File

@@ -58,6 +58,8 @@ body {
align-items: center;
gap: 1.25rem;
justify-content: center;
min-width: 0;
overflow: hidden;
}
/* Indivdual Link Styles */