diff --git a/pom.xml b/pom.xml index a80420a..ac0c9aa 100644 --- a/pom.xml +++ b/pom.xml @@ -125,6 +125,22 @@ org.springframework.boot spring-boot-starter-security-oauth2-client + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + testcontainers-junit-jupiter + test + + + org.testcontainers + testcontainers-postgresql + test + + @@ -132,14 +148,38 @@ org.apache.maven.plugins maven-compiler-plugin - - - - org.projectlombok - lombok - - - + + + default-compile + compile + + compile + + + + + org.projectlombok + lombok + + + + + + default-testCompile + test-compile + + testCompile + + + + + org.projectlombok + lombok + + + + + org.springframework.boot diff --git a/src/main/java/org/example/crimearchive/controllers/AccountsController.java b/src/main/java/org/example/crimearchive/controllers/AccountsController.java index 43b64e3..d55705d 100644 --- a/src/main/java/org/example/crimearchive/controllers/AccountsController.java +++ b/src/main/java/org/example/crimearchive/controllers/AccountsController.java @@ -6,6 +6,7 @@ import org.example.crimearchive.exceptions.PasswordValidationException; import org.example.crimearchive.polis.Account; import org.example.crimearchive.polis.UserService; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -34,6 +35,7 @@ public String adminPage(@AuthenticationPrincipal Account user, Model model) { } @GetMapping("/accounts/detail") + @PreAuthorize("#userId != authentication.principal.id") public String accountDetails(@RequestParam Long userId, @AuthenticationPrincipal Account user, Model model) { @@ -46,7 +48,7 @@ public String accountDetails(@RequestParam Long userId, } @PostMapping("/accounts/detail") - public String updateAccountsDetails(@ModelAttribute("updateAccount") DTOUpdatePolis updateAccount, + public String updateAccountsDetails(@ModelAttribute("updateAccount") @Valid DTOUpdatePolis updateAccount, BindingResult bindingResult, @AuthenticationPrincipal Account user, Model model) { @@ -61,6 +63,9 @@ public String updateAccountsDetails(@ModelAttribute("updateAccount") DTOUpdatePo } catch (IllegalArgumentException e) { bindingResult.rejectValue("roles", "error.updateAccount", e.getMessage()); return "updateaccountpage"; + }catch (PasswordValidationException e){ + bindingResult.rejectValue("password", "error.updateAccount", e.getMessage()); + return "updateaccountpage"; } return "redirect:/accounts"; } diff --git a/src/test/java/org/example/crimearchive/CrimeArchiveApplicationTests.java b/src/test/java/org/example/crimearchive/CrimeArchiveApplicationTests.java index 0ef3578..da4c8e6 100644 --- a/src/test/java/org/example/crimearchive/CrimeArchiveApplicationTests.java +++ b/src/test/java/org/example/crimearchive/CrimeArchiveApplicationTests.java @@ -8,7 +8,6 @@ class CrimeArchiveApplicationTests { @Test - @Disabled("Aktiveras när integrationstester är uppsatta") void contextLoads() { } } diff --git a/src/test/java/org/example/crimearchive/controllers/AccountControllerTests.java b/src/test/java/org/example/crimearchive/controllers/AccountControllerTests.java new file mode 100644 index 0000000..3f44325 --- /dev/null +++ b/src/test/java/org/example/crimearchive/controllers/AccountControllerTests.java @@ -0,0 +1,283 @@ +package org.example.crimearchive.controllers; + +import org.example.crimearchive.DTO.Polis.DTOCreatePolis; +import org.example.crimearchive.DTO.Polis.DTOUpdatePolis; +import org.example.crimearchive.polis.Account; +import org.junit.jupiter.api.Test; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.hamcrest.Matchers.*; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +public class AccountControllerTests extends IntegrationBaseTest { + + + /** + * Test uses Test container with report and account initializer + * WithUserDetails searchers the database for the username and uses that account in the test. + * + * @throws Exception When shit hits the fan. + */ + @Test + @WithUserDetails("sysadmin") + void adminShouldSeeAccountPage() throws Exception { + mockMvc.perform(get("/accounts")) + //Safety + .andExpect(status().isOk()) + //View + .andExpect(view().name("accountoverview")) + //Model + .andExpect(model().attributeExists("accountoverview", "currentUser", "allAccounts")) + .andExpect(model().attribute("accountoverview", true)) + // Correct Import here is Hamcrest isCollectionWithSize otherwise gets the objects as a list + .andExpect(model().attribute("allAccounts", hasSize(greaterThan(0)))) + .andExpect(model().attribute("allAccounts", not(hasItem(hasProperty("username", is("sysadmin")))))); + + } + + @Test + void nonAdminShoudNotGetAccessToAccountPage() throws Exception { + Account nonAdmin = createAndSaveTestUser("user", "user"); + + mockMvc.perform(get("/accounts") + .with(user(nonAdmin))) + .andExpect(status().isForbidden()); + } + + @Test + void adminCannotElevatePrivilegesOnSelf() throws Exception { + Account myAdmin = createAndSaveTestUser("admin", "admin"); + + mockMvc.perform(get("/accounts/detail") + .with(user(myAdmin)) + .param("userId", myAdmin.getId().toString())) + .andExpect(status().isForbidden()); + } + + @Test + void adminCanAccessAccountDetailsOnOthers() throws Exception { + Account myAdmin = createAndSaveTestUser("admin", "admin"); + Account myUser = createAndSaveTestUser("user", "user"); + + mockMvc.perform(get("/accounts/detail") + .with(user(myAdmin)) + .with(csrf()) + .param("userId", myUser.getId().toString())) + .andExpect(status().isOk()) + + .andExpect(view().name("updateaccountpage")) + + .andExpect(model().attribute("updateAccount", isA(DTOUpdatePolis.class))); + } + + @Test + @Transactional + void adminCanChangePrivlagesOnOthers() throws Exception { + Account myAdmin = createAndSaveTestUser("admin", "admin"); + Account myUser = createAndSaveTestUser("user", "user"); + + mockMvc.perform(post("/accounts/detail") + .with(user(myAdmin)) + .with(csrf()) + .param("id", myUser.getId().toString()) + .param("fullName", myUser.getFullName()) + .param("profession", myUser.getProfession()) + .param("department", myUser.getDepartment()) + .param("username", myUser.getUsername()) + .param("roles", "user,handler")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/accounts")); + + Account updatedUser = userRepository.findById(myUser.getId()).get(); + assertEquals(List.of("USER", "HANDLER"), updatedUser.getAuthoritesAsStringList()); + } + + @Test + void adminCanGoToNewAccountPage() throws Exception { + Account myAdmin = createAndSaveTestUser("admin", "admin"); + + mockMvc.perform(get("/accounts/add") + .with(user(myAdmin))) + .andExpect(status().isOk()) + + .andExpect(view().name("newaccountpage")) + + .andExpect(model().attribute("createAccount", isA(DTOCreatePolis.class))); + } + + @Test + void nonOtherThanAdminCanAccessCreateAccoutPage() throws Exception { + Account myUser = createAndSaveTestUser("user", "user"); + Account myHandler = createAndSaveTestUser("handler", "handler"); + Account accNoRoles = createAndSaveTestUser("noroles", ""); + + mockMvc.perform(get("/accounts/add") + .with(user(myUser))) + .andExpect(status().isForbidden()); + + mockMvc.perform(get("/accounts/add") + .with(user(myHandler))) + .andExpect(status().isForbidden()); + + mockMvc.perform(get("/accounts/add") + .with(user(accNoRoles))) + .andExpect(status().isForbidden()); + } + + @Test + @Transactional + void adminCanSaveNewAccounts() throws Exception { + Account myAdmin = createAndSaveTestUser("admin", "admin"); + + mockMvc.perform(post("/accounts/add") + .with(user(myAdmin)) + .with(csrf()) + .param("fullName", "name") + .param("profession", "pro") + .param("department", "dep") + .param("username", "username") + .param("password", "password") + .param("roles", "user")) + .andExpect(status().is3xxRedirection()); + + Account savedAcc = userRepository.findUserByUsername("username"); + assertEquals("name", savedAcc.getFullName()); + } + + @Test + void nonAdminCannotPostNewAccount() throws Exception { + Account myUser = createAndSaveTestUser("user", "user"); + + mockMvc.perform(post("/accounts/add") + .with(user(myUser)) + .with(csrf()) + .param("fullName", "name") + .param("profession", "pro") + .param("department", "dep") + .param("username", "username") + .param("password", "password") + .param("roles", "user")) + .andExpect(status().isForbidden()); + + } + + @Test + void updateAccountWithInvalidInputdontRedirect() throws Exception { + Account myAdmin = createAndSaveTestUser("admin", "admin"); + Account updateAcc = createAndSaveTestUser("user", "user"); + + mockMvc.perform(post("/accounts/detail") + .with(user(myAdmin)) + .with(csrf()) + .param("id", updateAcc.getId().toString()) + .param("fullName", "") + .param("profession", updateAcc.getProfession()) + .param("department", updateAcc.getDepartment()) + .param("username", updateAcc.getUsername()) + .param("roles", "user")) + .andExpect(status().isOk()) + .andExpect(view().name("updateaccountpage")); + } + + + @Test + void updateAccountWithNonExistantRolesDontRedirect() throws Exception { + Account myAdmin = createAndSaveTestUser("admin", "admin"); + Account updateAcc = createAndSaveTestUser("user", "user"); + + mockMvc.perform(post("/accounts/detail") + .with(user(myAdmin)) + .with(csrf()) + .param("id", updateAcc.getId().toString()) + .param("fullName", "newname") + .param("profession", updateAcc.getProfession()) + .param("department", updateAcc.getDepartment()) + .param("username", updateAcc.getUsername()) + .param("roles", "noSuchRole")) + .andExpect(status().isOk()) + .andExpect(view().name("updateaccountpage")); + } + + @Test + void updateAccountWithInvalidPasswordDontRedirect() throws Exception { + Account myAdmin = createAndSaveTestUser("admin", "admin"); + Account updateAcc = createAndSaveTestUser("user", "user"); + + mockMvc.perform(post("/accounts/detail") + .with(user(myAdmin)) + .with(csrf()) + .param("id", updateAcc.getId().toString()) + .param("fullName", "newname") + .param("profession", updateAcc.getProfession()) + .param("department", updateAcc.getDepartment()) + .param("username", updateAcc.getUsername()) + .param("password", "eror") + .param("roles", "user")) + .andExpect(status().isOk()) + .andExpect(view().name("updateaccountpage")); + } + @Test + void newAccountWithInvalidInputdontRedirect() throws Exception { + Account myAdmin = createAndSaveTestUser("admin", "admin"); + Account updateAcc = createAndSaveTestUser("user", "user"); + + mockMvc.perform(post("/accounts/add") + .with(user(myAdmin)) + .with(csrf()) + .param("id", updateAcc.getId().toString()) + .param("fullName", "") + .param("profession", updateAcc.getProfession()) + .param("department", updateAcc.getDepartment()) + .param("username", updateAcc.getUsername()) + .param("roles", "user")) + .andExpect(status().isOk()) + .andExpect(view().name("newaccountpage")); + } + + + @Test + void newAccountWithNonExistantRolesDontRedirect() throws Exception { + Account myAdmin = createAndSaveTestUser("admin", "admin"); + Account updateAcc = createAndSaveTestUser("user", "user"); + + mockMvc.perform(post("/accounts/add") + .with(user(myAdmin)) + .with(csrf()) + .param("id", updateAcc.getId().toString()) + .param("fullName", "newname") + .param("profession", updateAcc.getProfession()) + .param("department", updateAcc.getDepartment()) + .param("username", updateAcc.getUsername()) + .param("roles", "noSuchRole")) + .andExpect(status().isOk()) + .andExpect(view().name("newaccountpage")); + } + + @Test + void newAccountWithInvalidPasswordDontRedirect() throws Exception { + Account myAdmin = createAndSaveTestUser("admin", "admin"); + Account updateAcc = createAndSaveTestUser("user", "user"); + + mockMvc.perform(post("/accounts/add") + .with(user(myAdmin)) + .with(csrf()) + .param("id", updateAcc.getId().toString()) + .param("fullName", "newname") + .param("profession", updateAcc.getProfession()) + .param("department", updateAcc.getDepartment()) + .param("username", updateAcc.getUsername()) + .param("password", "eror") + .param("roles", "user")) + .andExpect(status().isOk()) + .andExpect(view().name("newaccountpage")); + } +} diff --git a/src/test/java/org/example/crimearchive/controllers/CaseAssigningControllerTests.java b/src/test/java/org/example/crimearchive/controllers/CaseAssigningControllerTests.java new file mode 100644 index 0000000..aad4050 --- /dev/null +++ b/src/test/java/org/example/crimearchive/controllers/CaseAssigningControllerTests.java @@ -0,0 +1,61 @@ +package org.example.crimearchive.controllers; + +import org.example.crimearchive.cases.Cases; +import org.example.crimearchive.polis.Account; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +public class CaseAssigningControllerTests extends IntegrationBaseTest { + + @Test + void handlerCanAccessCases() throws Exception { + Account myHandler = createAndSaveTestUser("handler", "handler"); + createCaseAndSave(); + mockMvc.perform(get("/cases") + .with(user(myHandler))) + .andExpect(status().isOk()) + .andExpect(view().name("cases")) + .andExpect(model().attribute("allUnsignedCases", hasSize(1))); + } + + @Test + void onlyHandlerCanAccessCases() throws Exception{ + Account myUser = createAndSaveTestUser("user", "user"); + Account myAdmin = createAndSaveTestUser("admin", "admin"); + + mockMvc.perform(get("/cases"). + with(user(myUser))) + .andExpect(status().isForbidden()); + mockMvc.perform(get("/cases") + .with(user(myAdmin))) + .andExpect(status().isForbidden()); + } + + @Test + void onlyHandlerCanAddAccountsToCase() throws Exception{ + Account myHandler = createAndSaveTestUser("handler", "handler"); + Account myUser = createAndSaveTestUser("user", "user"); + Cases testCase = createCaseAndSave(); + String kNumber = testCase.getCaseNumber(); + + mockMvc.perform(post("/cases/add") + .with(user(myHandler)) + .with(csrf()) + .param("addAccountId", myUser.getId().toString()) + .param("case_number", kNumber)) + .andExpect(status().is3xxRedirection()); + + mockMvc.perform(post("/cases/add") + .with(user(myUser)) + .with(csrf()) + .param("addAccountId", myUser.getId().toString()) + .param("case_number", kNumber)) + .andExpect(status().isForbidden()); + } +} diff --git a/src/test/java/org/example/crimearchive/controllers/IntegrationBaseTest.java b/src/test/java/org/example/crimearchive/controllers/IntegrationBaseTest.java new file mode 100644 index 0000000..21ff135 --- /dev/null +++ b/src/test/java/org/example/crimearchive/controllers/IntegrationBaseTest.java @@ -0,0 +1,62 @@ +package org.example.crimearchive.controllers; + +import org.example.crimearchive.KNumberService; +import org.example.crimearchive.cases.Cases; +import org.example.crimearchive.cases.CasesRepository; +import org.example.crimearchive.polis.Account; +import org.example.crimearchive.polis.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.postgresql.PostgreSQLContainer; +import software.amazon.awssdk.services.s3.S3Client; + +import java.util.List; + +@SpringBootTest +@Testcontainers +@AutoConfigureMockMvc +@Transactional +public abstract class IntegrationBaseTest { + + @ServiceConnection + protected static final PostgreSQLContainer postgres = new PostgreSQLContainer("postgres:18-alpine"); + + @Autowired + protected MockMvc mockMvc; + @Autowired + protected UserRepository userRepository; + @Autowired + protected CasesRepository casesRepository; + @Autowired + protected KNumberService Kservice; + @Autowired + protected PasswordEncoder encoder; + @MockitoBean + protected S3Client minio; + + + protected Account createAndSaveTestUser(String username, String role) { + Account user = new Account(); + user.setUsername(username); + user.setPassword(encoder.encode("password")); + user.setAuthorities(List.of(role)); + user.setFullName("Test gubbe"); + user.setProfession("PJ"); + user.setDepartment("Home"); + return userRepository.save(user); + } + + protected Cases createCaseAndSave(){ + Cases newCase = new Cases(); + newCase.setCaseNumber(Kservice.getKNumber()); + return casesRepository.save(newCase); + } +} diff --git a/src/test/java/org/example/crimearchive/controllers/ProfileControllerTests.java b/src/test/java/org/example/crimearchive/controllers/ProfileControllerTests.java new file mode 100644 index 0000000..86b465c --- /dev/null +++ b/src/test/java/org/example/crimearchive/controllers/ProfileControllerTests.java @@ -0,0 +1,98 @@ +package org.example.crimearchive.controllers; + +import org.example.crimearchive.DTO.Polis.DTOUpdateProfile; +import org.example.crimearchive.cases.Cases; +import org.example.crimearchive.polis.Account; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.Matchers.isA; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +public class ProfileControllerTests extends IntegrationBaseTest{ + + @Test + void onlyLoggedinWithAtLeastUserCanAccess() throws Exception{ + Account myUser = createAndSaveTestUser("user","user"); + + mockMvc.perform(get("/profile")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/login")); + mockMvc.perform(get("/profile") + .with(user(myUser))) + .andExpect(status().isOk()) + .andExpect(view().name("profile")); + } + + @Test + void userUpdateWithValidInputRedirects() throws Exception{ + Account myUser = createAndSaveTestUser("user", "user"); + + mockMvc.perform(post("/profile/update") + .with(user(myUser)) + .with(csrf()) + .param("fullname", "testsson") + .param("password", "newpassword")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/profile")); + + Account updatedUser = userRepository.findById(myUser.getId()).get(); + assertEquals("testsson", updatedUser.getFullName()); + } + + @Test + void userUpdateWithInvalidNameReturnsPage() throws Exception{ + Account myUser = createAndSaveTestUser("user", "user"); + + mockMvc.perform(post("/profile/update") + .with(user(myUser)) + .with(csrf()) + .param("fullname", "")) + .andExpect(status().isOk()) + .andExpect(view().name("profile")) + .andExpect(model().attribute("updateProfile", isA(DTOUpdateProfile.class))); + + } + + @Test + void userUpdateWithInvalidPasswordReturnsPage() throws Exception{ + Account myUser = createAndSaveTestUser("user", "user"); + + mockMvc.perform(post("/profile/update") + .with(user(myUser)) + .with(csrf()) + .param("fullname", "Testsson") + .param("password", "eror")) + .andExpect(status().isOk()) + .andExpect(view().name("profile")) + .andExpect(model().attribute("updateProfile", isA(DTOUpdateProfile.class))); + } + + @Test + void userWithCaseAccessCanViewCaseoverview() throws Exception{ + Account caseAccess = createAndSaveTestUser("user","user"); + Cases testCase = createCaseAndSave(); + testCase.addAccountToCase(caseAccess); + + mockMvc.perform(get("/caseoverview") + .with(user(caseAccess)) + .param("casenumber", testCase.getCaseNumber())) + .andExpect(status().isOk()) + .andExpect(view().name("caseoverview")); + } + + @Test + void userWithoutCaseAccessCannotViewCaseoverview() throws Exception{ + Account caseAccess = createAndSaveTestUser("user","user"); + Cases testCase = createCaseAndSave(); + + mockMvc.perform(get("/caseoverview") + .with(user(caseAccess)) + .param("casenumber", testCase.getCaseNumber())) + .andExpect(status().isForbidden()); + } +}