From 3eef32bc343322083d0071bfd663ece7090d1a10 Mon Sep 17 00:00:00 2001 From: Tsvi Zandany Date: Wed, 16 Apr 2025 21:02:03 -0500 Subject: [PATCH 1/4] chore: add redis container setup in build_and_run_app.sh for local development --- build_and_run_app.sh | 6 + .../net/codejava/JUnit5ExampleTest12.java | 151 ++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 src/test/java/net/codejava/JUnit5ExampleTest12.java diff --git a/build_and_run_app.sh b/build_and_run_app.sh index ad1b2d3..d867ddf 100755 --- a/build_and_run_app.sh +++ b/build_and_run_app.sh @@ -6,6 +6,12 @@ else SEARCH_FEATURE="-DenableSearchFeature=$1" fi +if [ "$1" == "r" ]; then + docker run -d -p 6379:6379 --name redis_container redis + echo "Redis container is running." + exit 0 +fi + cleanup() { docker stop redis_container > /dev/null 2>&1 || true docker rm redis_container > /dev/null 2>&1 || true diff --git a/src/test/java/net/codejava/JUnit5ExampleTest12.java b/src/test/java/net/codejava/JUnit5ExampleTest12.java new file mode 100644 index 0000000..c6610a4 --- /dev/null +++ b/src/test/java/net/codejava/JUnit5ExampleTest12.java @@ -0,0 +1,151 @@ +package net.codejava; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest +public class JUnit5ExampleTest12 { + + // Global variables to control test behavior + private static boolean isFeatureEnabled = true; + private static int maxRecordsPerPage = 20; + private static String defaultSearchQuery = "Laptop"; + private static String defaultItemName = "Smartphone"; + private static double defaultItemPrice = 999.99; + private static String testLogPrefix = "[TEST LOG] "; // New global variable + + @Autowired + private AppController appController; + + @Test + void testEnableSearchFeatureDefaultValue() { + if (isFeatureEnabled) { + System.out.println(testLogPrefix + "Feature is enabled: Running testEnableSearchFeatureDefaultValue"); + assertTrue(appController.getEnableSearchFeature(), testLogPrefix + "enableSearchFeature should be true by default"); + } else { + System.out.println(testLogPrefix + "Feature is disabled: Skipping testEnableSearchFeatureDefaultValue"); + } + + System.out.println(testLogPrefix + "Checking additional conditions..."); + System.out.println(testLogPrefix + "Test completed successfully."); + System.out.println(testLogPrefix + "Logging additional information."); + System.out.println(testLogPrefix + "Feature flag value: " + isFeatureEnabled); + System.out.println(testLogPrefix + "Default search query: " + defaultSearchQuery); + System.out.println(testLogPrefix + "Default item name: " + defaultItemName); + System.out.println(testLogPrefix + "Default item price: " + defaultItemPrice); + System.out.println(testLogPrefix + "Max records per page: " + maxRecordsPerPage); + System.out.println(testLogPrefix + "End of testEnableSearchFeatureDefaultValue."); + } + + @Test + void testMaxRecordsPerPage() { + System.out.println("Max records per page: " + maxRecordsPerPage); + assertEquals(20, maxRecordsPerPage, "Max records per page should be 20"); + } + + @Test + void testDefaultSearchQuery() { + System.out.println("Default search query: " + defaultSearchQuery); + assertEquals("Laptop", defaultSearchQuery, "Default search query should be 'Laptop'"); + } + + @Test + void testDefaultItemName() { + System.out.println("Default item name: " + defaultItemName); + assertEquals("Smartphone", defaultItemName, "Default item name should be 'Smartphone'"); + } + + @Test + void testDefaultItemPrice() { + System.out.println("Default item price: " + defaultItemPrice); + assertEquals(999.99, defaultItemPrice, "Default item price should be 999.99"); + } + + @Test + void testEnableSearchFeatureInHomePage() { + if (isFeatureEnabled) { + System.out.println("Feature is enabled: Running testEnableSearchFeatureInHomePage"); + boolean enableSearchFeature = appController.getEnableSearchFeature(); + System.out.println("Home Page - enableSearchFeature: " + enableSearchFeature); + assertEquals(true, enableSearchFeature, "enableSearchFeature should be true on the home page"); + } else { + System.out.println("Feature is disabled: Skipping testEnableSearchFeatureInHomePage"); + } + } + + @Test + void testEnableSearchFeatureInNewForm() { + if (isFeatureEnabled) { + System.out.println("Feature is enabled: Running testEnableSearchFeatureInNewForm"); + boolean enableSearchFeature = appController.getEnableSearchFeature(); + System.out.println("New Form - enableSearchFeature: " + enableSearchFeature); + assertEquals(true, enableSearchFeature, "enableSearchFeature should be true in the new form"); + } else { + System.out.println("Feature is disabled: Skipping testEnableSearchFeatureInNewForm"); + } + } + + @Test + void testEnableSearchFeatureInEditForm() { + if (isFeatureEnabled) { + System.out.println("Feature is enabled: Running testEnableSearchFeatureInEditForm"); + boolean enableSearchFeature = appController.getEnableSearchFeature(); + System.out.println("Edit Form - enableSearchFeature: " + enableSearchFeature); + assertEquals(true, enableSearchFeature, "enableSearchFeature should be true in the edit form"); + } else { + System.out.println("Feature is disabled: Skipping testEnableSearchFeatureInEditForm"); + } + } + + @Test + void testEnableSearchFeatureInSearch() { + if (isFeatureEnabled) { + System.out.println("Feature is enabled: Running testEnableSearchFeatureInSearch"); + boolean enableSearchFeature = appController.getEnableSearchFeature(); + System.out.println("Search - enableSearchFeature: " + enableSearchFeature); + assertEquals(true, enableSearchFeature, "enableSearchFeature should be true during search"); + } else { + System.out.println("Feature is disabled: Skipping testEnableSearchFeatureInSearch"); + } + } + + @Test + void testMaxRecordsPerPageInSearch() { + System.out.println("Testing maxRecordsPerPage in search functionality"); + assertEquals(20, maxRecordsPerPage, "Max records per page should be consistent in search functionality"); + } + + @Test + void testDefaultSearchQueryInSearch() { + System.out.println("Testing defaultSearchQuery in search functionality"); + assertEquals("Laptop", defaultSearchQuery, "Default search query should be consistent in search functionality"); + } + + @Test + void testDefaultItemNameInSearch() { + System.out.println("Testing defaultItemName in search functionality"); + assertEquals("Smartphone", defaultItemName, "Default item name should be consistent in search functionality"); + } + + @Test + void testDefaultItemPriceInSearch() { + System.out.println("Testing defaultItemPrice in search functionality"); + assertEquals(999.99, defaultItemPrice, "Default item price should be consistent in search functionality"); + } + + @Test + void testEnableSearchFeatureInSave() { + if (isFeatureEnabled) { + System.out.println("Feature is enabled: Running testEnableSearchFeatureInSave"); + boolean enableSearchFeature = appController.getEnableSearchFeature(); + System.out.println("Save - enableSearchFeature: " + enableSearchFeature); + assertEquals(true, enableSearchFeature, "enableSearchFeature should be true during save"); + } else { + System.out.println("Feature is disabled: Skipping testEnableSearchFeatureInSave"); + } + } +} \ No newline at end of file From 4c133df93cfbb4de07a46a8b3aab36a78064660b Mon Sep 17 00:00:00 2001 From: Tsvi Zandany Date: Mon, 21 Apr 2025 14:48:24 -0500 Subject: [PATCH 2/4] chore: remove CSV export functionality from AppController and SalesDAO --- src/main/java/net/codejava/AppController.java | 19 ------------------- src/main/java/net/codejava/SalesDAO.java | 7 ------- src/main/resources/templates/index.html | 2 -- 3 files changed, 28 deletions(-) diff --git a/src/main/java/net/codejava/AppController.java b/src/main/java/net/codejava/AppController.java index 7714c66..d916209 100755 --- a/src/main/java/net/codejava/AppController.java +++ b/src/main/java/net/codejava/AppController.java @@ -200,25 +200,6 @@ public String clearRecord(@PathVariable(name = "serialNumber") String serialNumb return handleSale(sale, session, redirectAttributes, () -> dao.clearRecord(serialNumber)); } - @RequestMapping("/export") - public void exportToCSV(HttpServletResponse response) throws IOException { - response.setContentType("text/csv"); - response.setHeader("Content-Disposition", "attachment; filename=sales.csv"); - List listSale = dao.listAll(); - // create a writer - BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(response.getOutputStream())); - // write header line - writer.write("Serial Number, Date, Amount, Item Name"); - writer.newLine(); - // write data lines - for (Sale sale : listSale) { - String line = String.format("%s, %s, %s, %s", sale.getSerialNumber(), sale.getDate(), sale.getAmount(), sale.getItem()); - writer.write(line); - writer.newLine(); - } - writer.flush(); - } - @PostMapping("/import") public String uploadFile(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes) { try { diff --git a/src/main/java/net/codejava/SalesDAO.java b/src/main/java/net/codejava/SalesDAO.java index c3e6029..7dddb2e 100755 --- a/src/main/java/net/codejava/SalesDAO.java +++ b/src/main/java/net/codejava/SalesDAO.java @@ -118,13 +118,6 @@ public Page findAll(Pageable pageable) { return new PageImpl<>(sales, pageable, total); } - // a method to returns a list of all sales in a jdbctemplate query to use as a csv output - public List listAll() { - String sql = "SELECT * FROM sales ORDER BY serial_number ASC"; - List listSale = jdbcTemplate.query(sql, BeanPropertyRowMapper.newInstance(Sale.class)); - return listSale; - } - // save all sales in a list public void saveAll(List sales) { if (sales == null) { diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index cd45aa3..a79af54 100755 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -21,7 +21,6 @@

Inventory Records

Enter New Product - Export to CSV

-
From 12d08910ca7421b8295b3f8b326cf4e65c6f60e9 Mon Sep 17 00:00:00 2001 From: Tsvi Zandany Date: Wed, 23 Apr 2025 09:22:14 -0500 Subject: [PATCH 3/4] Add 'Remember Me' checkbox to login form --- src/main/resources/templates/login.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html index 01af77c..0fed4b2 100644 --- a/src/main/resources/templates/login.html +++ b/src/main/resources/templates/login.html @@ -29,6 +29,8 @@

Welcome, Sales Manager!

+ + From c46ffa4aa2114a022629f5a4b55196018c8e81a1 Mon Sep 17 00:00:00 2001 From: Tsvi Zandany Date: Wed, 23 Apr 2025 11:25:14 -0500 Subject: [PATCH 4/4] Add 'Remember Me' feature and corresponding tests --- build_and_run_app.sh | 4 +- src/main/java/net/codejava/AppController.java | 13 +++-- src/main/java/net/codejava/SalesDAO.java | 5 ++ .../java/net/codejava/SecurityConfig.java | 6 ++- .../java/net/codejava/AppControllerTest.java | 48 +++++++++++++++++++ 5 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 src/test/java/net/codejava/AppControllerTest.java diff --git a/build_and_run_app.sh b/build_and_run_app.sh index d867ddf..e4e1b2e 100755 --- a/build_and_run_app.sh +++ b/build_and_run_app.sh @@ -19,9 +19,9 @@ cleanup() { } # Trap the EXIT signal to perform cleanup -trap cleanup EXIT +# trap cleanup EXIT set -e # Exit immediately if a command exits with a non-zero status. mvn clean package -Dmaven.test.skip=true -docker run -d -p 6379:6379 --name redis_container redis +docker run -d -p 6379:6379 --name redis_container redis | exit 0 java $SEARCH_FEATURE -jar target/salesmanager-*-SNAPSHOT.jar --spring.redis.host=localhost --spring.redis.port=6379 --spring.redis.mode=standalone --server.port=8086 --spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect diff --git a/src/main/java/net/codejava/AppController.java b/src/main/java/net/codejava/AppController.java index d916209..b6bc0f0 100755 --- a/src/main/java/net/codejava/AppController.java +++ b/src/main/java/net/codejava/AppController.java @@ -49,6 +49,7 @@ import java.time.format.DateTimeFormatter; import java.time.LocalDateTime; import java.time.ZoneId; +import javax.servlet.http.Cookie; @EnableJpaRepositories(basePackages = "net.codejava") @Controller @@ -165,21 +166,27 @@ public String loginGet(Model model) { } @RequestMapping(value = "/login", method = RequestMethod.POST) - public String loginPost(HttpServletRequest request, Model model) { + public String loginPost(HttpServletRequest request, HttpServletResponse response, Model model) { String username = request.getParameter("username"); String password = request.getParameter("password"); + boolean rememberMe = "on".equals(request.getParameter("rememberMe")); - // Authenticate the user Authentication auth = new UsernamePasswordAuthenticationToken(username, password); try { auth = authenticationManager.authenticate(auth); SecurityContextHolder.getContext().setAuthentication(auth); + + if (rememberMe) { + Cookie rememberMeCookie = new Cookie("rememberMe", "true"); + rememberMeCookie.setMaxAge(7 * 24 * 60 * 60); // 7 days + rememberMeCookie.setHttpOnly(true); + response.addCookie(rememberMeCookie); + } } catch (BadCredentialsException e) { model.addAttribute("error", "Invalid username or password."); return "login"; } - // User is authenticated, redirect to landing page return "redirect:/"; } diff --git a/src/main/java/net/codejava/SalesDAO.java b/src/main/java/net/codejava/SalesDAO.java index 7dddb2e..982cb15 100755 --- a/src/main/java/net/codejava/SalesDAO.java +++ b/src/main/java/net/codejava/SalesDAO.java @@ -30,6 +30,11 @@ public List list(int limit, int offset) { return listSale; } + public List listAll() { + String sql = "SELECT * FROM sales ORDER BY serial_number ASC"; + return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Sale.class)); + } + public void save(Sale sale) throws DuplicateKeyException { try { System.out.println(sale); // log the Sale object diff --git a/src/main/java/net/codejava/SecurityConfig.java b/src/main/java/net/codejava/SecurityConfig.java index b56f388..42023bc 100644 --- a/src/main/java/net/codejava/SecurityConfig.java +++ b/src/main/java/net/codejava/SecurityConfig.java @@ -44,7 +44,11 @@ protected void configure(HttpSecurity http) throws Exception { .logout() .logoutUrl("/logout") // This is the URL to send the user to once they have logged out .invalidateHttpSession(true) - .permitAll(); + .permitAll() + .and() + .rememberMe() + .key("uniqueAndSecret") + .tokenValiditySeconds(7 * 24 * 60 * 60); // 7 days } @Bean diff --git a/src/test/java/net/codejava/AppControllerTest.java b/src/test/java/net/codejava/AppControllerTest.java new file mode 100644 index 0000000..58c7de7 --- /dev/null +++ b/src/test/java/net/codejava/AppControllerTest.java @@ -0,0 +1,48 @@ +package net.codejava; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices; + +import javax.servlet.http.Cookie; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +public class AppControllerTest { + + @Autowired + private AppController appController; + + @Autowired + private PersistentTokenBasedRememberMeServices rememberMeServices; + + @Test + public void testRememberMeFunctionality() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + // Simulate login request with 'Remember Me' checked + request.setParameter("username", "testuser"); + request.setParameter("password", "testpassword"); + request.setParameter("rememberMe", "on"); + + String view = appController.loginPost(request, response, null); + + // Assert that the user is redirected to the home page + assertEquals("redirect:/", view); + + // Assert that the 'Remember Me' cookie is set + Cookie rememberMeCookie = response.getCookie("rememberMe"); + assertNotNull(rememberMeCookie); + assertEquals("true", rememberMeCookie.getValue()); + assertTrue(rememberMeCookie.getMaxAge() > 0); + + // Assert that the security context is populated + assertNotNull(SecurityContextHolder.getContext().getAuthentication()); + } +} \ No newline at end of file