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());
+ }
+}