From 2df9e64b19e2d3354de2fbe49dc9e45dab8ac65d Mon Sep 17 00:00:00 2001 From: Matthew Mandell Date: Thu, 20 Nov 2025 16:43:10 -0800 Subject: [PATCH 1/7] replaced example with title --- frontend/index.html | 2 +- frontend/src/main/components/Nav/AppNavbar.jsx | 2 +- frontend/src/tests/indexHtml.test.jsx | 11 +++++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 frontend/src/tests/indexHtml.test.jsx diff --git a/frontend/index.html b/frontend/index.html index d518aeec..40580344 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - Vite + React + UCSB Rec
diff --git a/frontend/src/main/components/Nav/AppNavbar.jsx b/frontend/src/main/components/Nav/AppNavbar.jsx index 17824854..55472008 100644 --- a/frontend/src/main/components/Nav/AppNavbar.jsx +++ b/frontend/src/main/components/Nav/AppNavbar.jsx @@ -25,7 +25,7 @@ export default function AppNavbar({ > - Example + UCSB Rec diff --git a/frontend/src/tests/indexHtml.test.jsx b/frontend/src/tests/indexHtml.test.jsx new file mode 100644 index 00000000..c032ca26 --- /dev/null +++ b/frontend/src/tests/indexHtml.test.jsx @@ -0,0 +1,11 @@ +import { readFileSync } from "fs"; +import { resolve } from "path"; +import { test, expect } from "vitest"; + +test("frontend/index.html has title 'UCSB Rec'", () => { + const indexPath = resolve(process.cwd(), "index.html"); + const html = readFileSync(indexPath, "utf-8"); + const m = html.match(/(.*?)<\/title>/i); + expect(m).not.toBeNull(); + expect(m[1]).toBe("UCSB Rec"); +}); From bfcc0a2863bd2eac2e79bdec217c91792b1dc5b4 Mon Sep 17 00:00:00 2001 From: Matthew Mandell <matthewmandell@ucsb.edu> Date: Thu, 20 Nov 2025 19:00:07 -0800 Subject: [PATCH 2/7] changed line in application-develepment.properties --- src/main/resources/application-development.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application-development.properties b/src/main/resources/application-development.properties index 3e3da24c..e9de84ee 100644 --- a/src/main/resources/application-development.properties +++ b/src/main/resources/application-development.properties @@ -1,6 +1,6 @@ logging.level.sql=DEBUG logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE -spring.datasource.url=jdbc:h2:file:./target/db-development +spring.datasource.url=jdbc:h2:file:./target/db-development;AUTO_SERVER=TRUE spring.datasource.username=sa spring.datasource.password=password spring.h2.console.settings.web-allow-others=true From cde6798094b0908981074412e88ac460f231a438 Mon Sep 17 00:00:00 2001 From: Matthew Mandell <matthewmandell@ucsb.edu> Date: Mon, 24 Nov 2025 14:10:09 -0800 Subject: [PATCH 3/7] added link to navbar to rec page --- .../src/main/components/Nav/AppNavbar.jsx | 5 ++ .../tests/components/Nav/AppNavbar.test.jsx | 55 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/frontend/src/main/components/Nav/AppNavbar.jsx b/frontend/src/main/components/Nav/AppNavbar.jsx index 17824854..94e68af3 100644 --- a/frontend/src/main/components/Nav/AppNavbar.jsx +++ b/frontend/src/main/components/Nav/AppNavbar.jsx @@ -49,6 +49,11 @@ export default function AppNavbar({ <Navbar.Collapse className="justify-content-between"> <Nav className="mr-auto"> + {currentUser && currentUser.loggedIn && ( + <Nav.Link as={Link} to="/requests/create"> + Request Recommendation + </Nav.Link> + )} {hasRole(currentUser, "ROLE_ADMIN") && ( <NavDropdown title="Admin" diff --git a/frontend/src/tests/components/Nav/AppNavbar.test.jsx b/frontend/src/tests/components/Nav/AppNavbar.test.jsx index 8b4efff0..3c333c5f 100644 --- a/frontend/src/tests/components/Nav/AppNavbar.test.jsx +++ b/frontend/src/tests/components/Nav/AppNavbar.test.jsx @@ -233,4 +233,59 @@ describe("AppNavbar tests", () => { expect(screen.queryByText("Completed Requests")).not.toBeInTheDocument(); expect(screen.queryByText("Statistics")).not.toBeInTheDocument(); }); + + test("renders Request Recommendation link for logged in users", async () => { + const currentUser = currentUserFixtures.userOnly; + const doLogin = vi.fn(); + + render( + <QueryClientProvider client={queryClient}> + <MemoryRouter> + <AppNavbar currentUser={currentUser} doLogin={doLogin} /> + </MemoryRouter> + </QueryClientProvider>, + ); + + await screen.findByText("Request Recommendation"); + const requestLink = screen.getByText("Request Recommendation"); + expect(requestLink).toBeInTheDocument(); + expect(requestLink).toHaveAttribute("href", "/requests/create"); + }); + + test("Request Recommendation link appears for professor users", async () => { + const currentUser = currentUserFixtures.professorUser; + const doLogin = vi.fn(); + + render( + <QueryClientProvider client={queryClient}> + <MemoryRouter> + <AppNavbar currentUser={currentUser} doLogin={doLogin} /> + </MemoryRouter> + </QueryClientProvider>, + ); + + await screen.findByText("Request Recommendation"); + const requestLink = screen.getByText("Request Recommendation"); + expect(requestLink).toBeInTheDocument(); + }); + + test("Request Recommendation link does not show when not logged in", async () => { + const currentUser = currentUserFixtures.notLoggedIn; + const systemInfo = systemInfoFixtures.showingBoth; + const doLogin = vi.fn(); + + render( + <QueryClientProvider client={queryClient}> + <MemoryRouter> + <AppNavbar + currentUser={currentUser} + systemInfo={systemInfo} + doLogin={doLogin} + /> + </MemoryRouter> + </QueryClientProvider>, + ); + + expect(screen.queryByText("Request Recommendation")).not.toBeInTheDocument(); + }); }); From 28f462add583ae425942234a90b7ded6dbeac7eb Mon Sep 17 00:00:00 2001 From: Matthew Mandell <matthewmandell@ucsb.edu> Date: Mon, 24 Nov 2025 14:22:13 -0800 Subject: [PATCH 4/7] Run prettier formatting --- frontend/src/tests/components/Nav/AppNavbar.test.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/tests/components/Nav/AppNavbar.test.jsx b/frontend/src/tests/components/Nav/AppNavbar.test.jsx index 3c333c5f..54314ae3 100644 --- a/frontend/src/tests/components/Nav/AppNavbar.test.jsx +++ b/frontend/src/tests/components/Nav/AppNavbar.test.jsx @@ -286,6 +286,8 @@ describe("AppNavbar tests", () => { </QueryClientProvider>, ); - expect(screen.queryByText("Request Recommendation")).not.toBeInTheDocument(); + expect( + screen.queryByText("Request Recommendation"), + ).not.toBeInTheDocument(); }); }); From cb1a8d91935d8e42759d0f5f73d39f8d83720899 Mon Sep 17 00:00:00 2001 From: Matthew Mandell <matthewmandell@ucsb.edu> Date: Mon, 24 Nov 2025 15:22:21 -0800 Subject: [PATCH 5/7] implemented loading of hardcoded request types at application startup --- .../ucsb/cs156/rec/ExampleApplication.java | 4 + .../rec/services/RequestTypeService.java | 52 ++++++++++++ .../rec/services/RequestTypeServiceTest.java | 79 +++++++++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 src/main/java/edu/ucsb/cs156/rec/services/RequestTypeService.java create mode 100644 src/test/java/edu/ucsb/cs156/rec/services/RequestTypeServiceTest.java diff --git a/src/main/java/edu/ucsb/cs156/rec/ExampleApplication.java b/src/main/java/edu/ucsb/cs156/rec/ExampleApplication.java index 6a3ffea2..b2346018 100644 --- a/src/main/java/edu/ucsb/cs156/rec/ExampleApplication.java +++ b/src/main/java/edu/ucsb/cs156/rec/ExampleApplication.java @@ -1,5 +1,6 @@ package edu.ucsb.cs156.rec; +import edu.ucsb.cs156.rec.services.RequestTypeService; import edu.ucsb.cs156.rec.services.wiremock.WiremockService; import java.time.ZonedDateTime; import java.util.Optional; @@ -21,6 +22,8 @@ public class ExampleApplication { @Autowired WiremockService wiremockService; + @Autowired RequestTypeService requestTypeService; + @Bean public DateTimeProvider utcDateTimeProvider() { return () -> { @@ -49,6 +52,7 @@ public ApplicationRunner wiremockApplicationRunner() { public ApplicationRunner developmentApplicationRunner() { return arg -> { log.info("development mode"); + requestTypeService.loadRequestTypes(); log.info("developmentApplicationRunner completed"); }; } diff --git a/src/main/java/edu/ucsb/cs156/rec/services/RequestTypeService.java b/src/main/java/edu/ucsb/cs156/rec/services/RequestTypeService.java new file mode 100644 index 00000000..390c7a0f --- /dev/null +++ b/src/main/java/edu/ucsb/cs156/rec/services/RequestTypeService.java @@ -0,0 +1,52 @@ +package edu.ucsb.cs156.rec.services; + +import edu.ucsb.cs156.rec.entities.RequestType; +import edu.ucsb.cs156.rec.repositories.RequestTypeRepository; +import java.util.Arrays; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** Service for managing Request Types. */ +@Service +@Slf4j +public class RequestTypeService { + + @Autowired private RequestTypeRepository requestTypeRepository; + + /** List of hardcoded request types to be loaded at startup. */ + private static final List<String> HARDCODED_REQUEST_TYPES = + Arrays.asList( + "CS Department BS/MS program", + "Scholarship or Fellowship", + "MS program (other than CS Dept BS/MS)", + "PhD program", + "Other"); + + /** + * Load hardcoded request types into the database if they don't already exist. + * + * <p>This method checks for each hardcoded request type and only creates it if it's not already + * in the database. + */ + public void loadRequestTypes() { + log.info("Loading hardcoded request types..."); + int loadedCount = 0; + int skippedCount = 0; + + for (String type : HARDCODED_REQUEST_TYPES) { + if (requestTypeRepository.findByRequestType(type).isEmpty()) { + RequestType requestType = RequestType.builder().requestType(type).build(); + requestTypeRepository.save(requestType); + log.info("Loaded request type: {}", type); + loadedCount++; + } else { + log.debug("Request type already exists, skipping: {}", type); + skippedCount++; + } + } + + log.info("Request type loading completed. Loaded: {}, Skipped: {}", loadedCount, skippedCount); + } +} diff --git a/src/test/java/edu/ucsb/cs156/rec/services/RequestTypeServiceTest.java b/src/test/java/edu/ucsb/cs156/rec/services/RequestTypeServiceTest.java new file mode 100644 index 00000000..21641ca5 --- /dev/null +++ b/src/test/java/edu/ucsb/cs156/rec/services/RequestTypeServiceTest.java @@ -0,0 +1,79 @@ +package edu.ucsb.cs156.rec.services; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import edu.ucsb.cs156.rec.entities.RequestType; +import edu.ucsb.cs156.rec.repositories.RequestTypeRepository; +import java.util.Optional; +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; + +@ExtendWith(MockitoExtension.class) +public class RequestTypeServiceTest { + + @Mock private RequestTypeRepository requestTypeRepository; + + @InjectMocks private RequestTypeService requestTypeService; + + @Test + public void test_loadRequestTypes_loadsAllTypesWhenNoneExist() { + // Arrange + when(requestTypeRepository.findByRequestType(any(String.class))).thenReturn(Optional.empty()); + + // Act + requestTypeService.loadRequestTypes(); + + // Assert + // Should save 5 request types: CS Department BS/MS program, Scholarship or Fellowship, + // MS program (other than CS Dept BS/MS), PhD program, Other + verify(requestTypeRepository, times(5)).save(any(RequestType.class)); + verify(requestTypeRepository, times(5)).findByRequestType(any(String.class)); + } + + @Test + public void test_loadRequestTypes_skipsExistingTypes() { + // Arrange + RequestType existingType = RequestType.builder().id(1L).requestType("Other").build(); + + // Mock that some types exist and some don't + when(requestTypeRepository.findByRequestType("CS Department BS/MS program")) + .thenReturn(Optional.empty()); + when(requestTypeRepository.findByRequestType("Scholarship or Fellowship")) + .thenReturn(Optional.of(existingType)); + when(requestTypeRepository.findByRequestType("MS program (other than CS Dept BS/MS)")) + .thenReturn(Optional.empty()); + when(requestTypeRepository.findByRequestType("PhD program")).thenReturn(Optional.empty()); + when(requestTypeRepository.findByRequestType("Other")).thenReturn(Optional.of(existingType)); + + // Act + requestTypeService.loadRequestTypes(); + + // Assert + // Should only save 3 new types (skipping "Scholarship or Fellowship" and "Other") + verify(requestTypeRepository, times(3)).save(any(RequestType.class)); + verify(requestTypeRepository, times(5)).findByRequestType(any(String.class)); + } + + @Test + public void test_loadRequestTypes_skipsAllWhenAllExist() { + // Arrange + RequestType existingType = RequestType.builder().id(1L).requestType("Some Type").build(); + + when(requestTypeRepository.findByRequestType(any(String.class))) + .thenReturn(Optional.of(existingType)); + + // Act + requestTypeService.loadRequestTypes(); + + // Assert + // Should not save any types since all already exist + verify(requestTypeRepository, times(0)).save(any(RequestType.class)); + verify(requestTypeRepository, times(5)).findByRequestType(any(String.class)); + } +} From b766d256f503e484097a06df5079df723518894b Mon Sep 17 00:00:00 2001 From: Matthew Mandell <matthewmandell@ucsb.edu> Date: Mon, 24 Nov 2025 15:37:08 -0800 Subject: [PATCH 6/7] added more extensive testing to pass mutation and jacoco tests --- src/main/java/edu/ucsb/cs156/rec/ExampleApplication.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/ucsb/cs156/rec/ExampleApplication.java b/src/main/java/edu/ucsb/cs156/rec/ExampleApplication.java index b2346018..70cf0f40 100644 --- a/src/main/java/edu/ucsb/cs156/rec/ExampleApplication.java +++ b/src/main/java/edu/ucsb/cs156/rec/ExampleApplication.java @@ -22,7 +22,8 @@ public class ExampleApplication { @Autowired WiremockService wiremockService; - @Autowired RequestTypeService requestTypeService; + @Autowired(required = false) + RequestTypeService requestTypeService; @Bean public DateTimeProvider utcDateTimeProvider() { @@ -52,7 +53,9 @@ public ApplicationRunner wiremockApplicationRunner() { public ApplicationRunner developmentApplicationRunner() { return arg -> { log.info("development mode"); - requestTypeService.loadRequestTypes(); + if (requestTypeService != null) { + requestTypeService.loadRequestTypes(); + } log.info("developmentApplicationRunner completed"); }; } From 4e869377226f1b21ff5a1916e0dc5b69e02c0bdd Mon Sep 17 00:00:00 2001 From: Matthew Mandell <matthewmandell@ucsb.edu> Date: Mon, 24 Nov 2025 15:47:38 -0800 Subject: [PATCH 7/7] finally got to 100 on mutationtests --- .../cs156/rec/services/RequestTypeService.java | 15 ++++++++++++++- .../rec/services/RequestTypeServiceTest.java | 13 ++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/ucsb/cs156/rec/services/RequestTypeService.java b/src/main/java/edu/ucsb/cs156/rec/services/RequestTypeService.java index 390c7a0f..6875ee3b 100644 --- a/src/main/java/edu/ucsb/cs156/rec/services/RequestTypeService.java +++ b/src/main/java/edu/ucsb/cs156/rec/services/RequestTypeService.java @@ -4,6 +4,8 @@ import edu.ucsb.cs156.rec.repositories.RequestTypeRepository; import java.util.Arrays; import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -24,13 +26,23 @@ public class RequestTypeService { "PhD program", "Other"); + /** Result class for loadRequestTypes method. */ + @Data + @AllArgsConstructor + public static class LoadResult { + private int loaded; + private int skipped; + } + /** * Load hardcoded request types into the database if they don't already exist. * * <p>This method checks for each hardcoded request type and only creates it if it's not already * in the database. + * + * @return LoadResult containing the number of types loaded and skipped */ - public void loadRequestTypes() { + public LoadResult loadRequestTypes() { log.info("Loading hardcoded request types..."); int loadedCount = 0; int skippedCount = 0; @@ -48,5 +60,6 @@ public void loadRequestTypes() { } log.info("Request type loading completed. Loaded: {}, Skipped: {}", loadedCount, skippedCount); + return new LoadResult(loadedCount, skippedCount); } } diff --git a/src/test/java/edu/ucsb/cs156/rec/services/RequestTypeServiceTest.java b/src/test/java/edu/ucsb/cs156/rec/services/RequestTypeServiceTest.java index 21641ca5..ec268ea7 100644 --- a/src/test/java/edu/ucsb/cs156/rec/services/RequestTypeServiceTest.java +++ b/src/test/java/edu/ucsb/cs156/rec/services/RequestTypeServiceTest.java @@ -1,5 +1,6 @@ package edu.ucsb.cs156.rec.services; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -27,13 +28,15 @@ public void test_loadRequestTypes_loadsAllTypesWhenNoneExist() { when(requestTypeRepository.findByRequestType(any(String.class))).thenReturn(Optional.empty()); // Act - requestTypeService.loadRequestTypes(); + RequestTypeService.LoadResult result = requestTypeService.loadRequestTypes(); // Assert // Should save 5 request types: CS Department BS/MS program, Scholarship or Fellowship, // MS program (other than CS Dept BS/MS), PhD program, Other verify(requestTypeRepository, times(5)).save(any(RequestType.class)); verify(requestTypeRepository, times(5)).findByRequestType(any(String.class)); + assertEquals(5, result.getLoaded()); + assertEquals(0, result.getSkipped()); } @Test @@ -52,12 +55,14 @@ public void test_loadRequestTypes_skipsExistingTypes() { when(requestTypeRepository.findByRequestType("Other")).thenReturn(Optional.of(existingType)); // Act - requestTypeService.loadRequestTypes(); + RequestTypeService.LoadResult result = requestTypeService.loadRequestTypes(); // Assert // Should only save 3 new types (skipping "Scholarship or Fellowship" and "Other") verify(requestTypeRepository, times(3)).save(any(RequestType.class)); verify(requestTypeRepository, times(5)).findByRequestType(any(String.class)); + assertEquals(3, result.getLoaded()); + assertEquals(2, result.getSkipped()); } @Test @@ -69,11 +74,13 @@ public void test_loadRequestTypes_skipsAllWhenAllExist() { .thenReturn(Optional.of(existingType)); // Act - requestTypeService.loadRequestTypes(); + RequestTypeService.LoadResult result = requestTypeService.loadRequestTypes(); // Assert // Should not save any types since all already exist verify(requestTypeRepository, times(0)).save(any(RequestType.class)); verify(requestTypeRepository, times(5)).findByRequestType(any(String.class)); + assertEquals(0, result.getLoaded()); + assertEquals(5, result.getSkipped()); } }