diff --git a/src/main/java/org/example/cyberwatch/config/DataInitializer.java b/src/main/java/org/example/cyberwatch/config/DataInitializer.java index 2b9a0c7..faef873 100644 --- a/src/main/java/org/example/cyberwatch/config/DataInitializer.java +++ b/src/main/java/org/example/cyberwatch/config/DataInitializer.java @@ -1,5 +1,6 @@ package org.example.cyberwatch.config; +import org.example.cyberwatch.config.security.EncryptionService; import org.example.cyberwatch.features.staff.model.Staff; import org.example.cyberwatch.features.staff.repository.StaffRepository; import org.example.cyberwatch.shared.model.enums.Department; @@ -14,10 +15,12 @@ public class DataInitializer implements CommandLineRunner { private final StaffRepository staffRepository; + private final EncryptionService encryptionService; private final PasswordEncoder passwordEncoder; - public DataInitializer(StaffRepository staffRepository, PasswordEncoder passwordEncoder) { + public DataInitializer(StaffRepository staffRepository, EncryptionService encryptionService, PasswordEncoder passwordEncoder) { this.staffRepository = staffRepository; + this.encryptionService = encryptionService; this.passwordEncoder = passwordEncoder; } @@ -28,15 +31,16 @@ public void run(String... args) { } staffRepository.saveAll(List.of( - createStaff("19900101-0101", "Alice", "Andersson", "alice@cyberwatch.local", "0701111111", Role.HR, Department.BACKEND, "testPass123"), - createStaff("19880505-0505", "Bob", "Berg", "bob@cyberwatch.local", "0702222222", Role.CEO, Department.DEVOPS, "testPass1234"), - createStaff("19770707-0707", "Carla", "Carlsson", "carla@cyberwatch.local", "0703333333", Role.CTO, Department.HR, "testPass12345") + createStaff("19900101-0101", "Eric", "Thilen", "ericthilen2003@gmail.com", "0701111111", Role.ADMIN, Department.BACKEND, "testPass123"), + createStaff("19900505-0505", "Caroline", "Nordbrandt", "nordbrandtcaroline@gmail.com", "0702222222", Role.ADMIN, Department.DEVOPS, "testPass1234"), + createStaff("19900707-0707", "Alice", "Wersen", "alicewersen@hotmail.com", "0703333333", Role.ADMIN, Department.HR, "testPass12345"), + createStaff("19900909-0909", "Younes", "Lamia", "younescool94@gmail.com", "0704444444", Role.ADMIN, Department.FRONTEND, "testPass1234") )); } private Staff createStaff(String ssn, String firstName, String lastName, String email, String phone, Role role, Department department, String rawPassword) { Staff staff = new Staff(); - staff.setSocialSecurityNumber(ssn); + staff.setSocialSecurityNumber(encryptionService.encrypt(ssn)); staff.setFirstName(firstName); staff.setLastName(lastName); staff.setEmail(email); diff --git a/src/main/java/org/example/cyberwatch/config/EncryptionConfig.java b/src/main/java/org/example/cyberwatch/config/EncryptionConfig.java new file mode 100644 index 0000000..e914265 --- /dev/null +++ b/src/main/java/org/example/cyberwatch/config/EncryptionConfig.java @@ -0,0 +1,22 @@ +package org.example.cyberwatch.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.encrypt.Encryptors; +import org.springframework.security.crypto.encrypt.TextEncryptor; + +@Configuration +public class EncryptionConfig { + + @Value("${app.encryption.key}") + private String encryptionKey; + + @Value("${app.encryption.salt}") + private String salt; + + @Bean + public TextEncryptor textEncryptor() { + return Encryptors.text(encryptionKey, salt); + } +} diff --git a/src/main/java/org/example/cyberwatch/config/security/EncryptionService.java b/src/main/java/org/example/cyberwatch/config/security/EncryptionService.java new file mode 100644 index 0000000..f614c89 --- /dev/null +++ b/src/main/java/org/example/cyberwatch/config/security/EncryptionService.java @@ -0,0 +1,31 @@ +package org.example.cyberwatch.config.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.encrypt.TextEncryptor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class EncryptionService { + + private final TextEncryptor textEncryptor; + + public String encrypt(String data) { + return textEncryptor.encrypt(data); + } + + public String decrypt(String data) { + return textEncryptor.decrypt(data); + } + + public String maskLastFour(String encrypted) { + if (encrypted == null || encrypted.isBlank()) { + return "****"; + } + String decrypted = textEncryptor.decrypt(encrypted); + if (decrypted.length() <= 4) { + return "****"; + } + return decrypted.substring(0, decrypted.length() - 4) + "****"; + } +} diff --git a/src/main/java/org/example/cyberwatch/features/form/controller/EmploymentFormController.java b/src/main/java/org/example/cyberwatch/features/form/controller/EmploymentFormController.java index e4119d7..3a907c4 100644 --- a/src/main/java/org/example/cyberwatch/features/form/controller/EmploymentFormController.java +++ b/src/main/java/org/example/cyberwatch/features/form/controller/EmploymentFormController.java @@ -5,11 +5,11 @@ import org.example.cyberwatch.features.form.dto.EmploymentFormDTO; import org.example.cyberwatch.features.form.dto.UpdateEmploymentDTO; import org.example.cyberwatch.features.form.service.EmploymentFormService; +import org.example.cyberwatch.features.staff.model.Staff; import org.example.cyberwatch.shared.model.enums.ApprovalStatus; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -28,25 +28,18 @@ public EmploymentFormController(EmploymentFormService employmentFormService) { @PostMapping("/employment") public ResponseEntity createEmploymentForm(@Valid @RequestBody CreateEmploymentDTO dto, - Authentication authentication) { - Object principal = authentication.getPrincipal(); - if (!(principal instanceof org.example.cyberwatch.features.staff.model.Staff staff)) { - throw new AccessDeniedException("Principal must be a Staff object"); - } - logger.info("Creating employment form for HR staff: {}", staff.getEmail()); + @AuthenticationPrincipal Staff staff) { + logger.info("Creating employment form by HR staff: {}", staff.getEmail()); EmploymentFormDTO createdForm = employmentFormService.createForm(dto, staff); return ResponseEntity.status(HttpStatus.CREATED).body(createdForm); } - + //Change returntype when emailservice is implemented @PostMapping("/{id}/approve") //Change returntype when emailservice is implemented - public ResponseEntity approveForm(@PathVariable Long id, Authentication authentication) { - Object principal = authentication.getPrincipal(); - if (!(principal instanceof org.example.cyberwatch.features.staff.model.Staff staff)) { - throw new AccessDeniedException("Principal must be a Staff object"); - } - logger.info("Creating employment form with approval from staff: {}", staff.getEmail()); + public ResponseEntity approveForm(@PathVariable Long id, @AuthenticationPrincipal Staff staff) { + + logger.info("Approving employment form {} by staff: {}", id, staff.getId()); return ResponseEntity.ok( employmentFormService.approveAndFinalizeEmployment(id, staff)); } @@ -55,11 +48,8 @@ public ResponseEntity approveForm(@PathVariable Long id, Authentication public ResponseEntity updateForm( @PathVariable Long id, @Valid @RequestBody UpdateEmploymentDTO dto, - Authentication auth) { - Object principal = auth.getPrincipal(); - if (!(principal instanceof org.example.cyberwatch.features.staff.model.Staff staff)) { - throw new AccessDeniedException("Principal must be a Staff object"); - } + @AuthenticationPrincipal Staff staff) { + EmploymentFormDTO updatedForm = employmentFormService.updateFormBeforeApproval(id, dto, staff); return ResponseEntity.ok(updatedForm); } @@ -80,12 +70,8 @@ public ResponseEntity getFormById(@PathVariable Long id) { @PostMapping("/{id}/reject") public ResponseEntity rejectForm( @PathVariable Long id, - Authentication authentication) { + @AuthenticationPrincipal Staff staff) { - Object principal = authentication.getPrincipal(); - if (!(principal instanceof org.example.cyberwatch.features.staff.model.Staff staff)) { - throw new AccessDeniedException("Principal must be a Staff object"); - } String message = employmentFormService.rejectForm(id, staff); return ResponseEntity.ok(message); @@ -95,11 +81,8 @@ public ResponseEntity rejectForm( @DeleteMapping("/{id}") public ResponseEntity deleteForm( @PathVariable Long id, - Authentication authentication) { - Object principal = authentication.getPrincipal(); - if (!(principal instanceof org.example.cyberwatch.features.staff.model.Staff staff)) { - throw new AccessDeniedException("Principal must be a Staff object"); - } + @AuthenticationPrincipal Staff staff) { + employmentFormService.deleteForm(id, staff); return ResponseEntity.noContent().build(); } diff --git a/src/main/java/org/example/cyberwatch/features/form/model/EmploymentForm.java b/src/main/java/org/example/cyberwatch/features/form/model/EmploymentForm.java index 221e4ad..f8def19 100644 --- a/src/main/java/org/example/cyberwatch/features/form/model/EmploymentForm.java +++ b/src/main/java/org/example/cyberwatch/features/form/model/EmploymentForm.java @@ -29,7 +29,6 @@ public class EmploymentForm { @Column(name = "social_security_number", nullable = false, unique = true) @NotBlank(message = "Social security number cannot be blank") - @Pattern(regexp = "\\d{6}-\\d{4}|\\d{8}-\\d{4}", message = "Social security number must be in format YYMMDD-NNNN or YYYYMMDD-NNNN") private String socialSecurityNumber; @Column(name = "first_name") diff --git a/src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java b/src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java index bea6cc6..483be5b 100644 --- a/src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java +++ b/src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.RandomStringUtils; +import org.example.cyberwatch.config.security.EncryptionService; import org.example.cyberwatch.features.form.dto.CreateEmploymentDTO; import org.example.cyberwatch.features.form.dto.EmploymentFormDTO; import org.example.cyberwatch.features.form.dto.UpdateEmploymentDTO; @@ -16,6 +17,7 @@ import org.example.cyberwatch.shared.model.enums.Role; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -38,6 +40,7 @@ public class EmploymentFormService { private final S3Service s3Service; private final ObjectMapper objectMapper; private final PasswordEncoder passwordEncoder; + private final EncryptionService encryptionService; //Create employment form @Transactional @@ -57,11 +60,14 @@ public EmploymentFormDTO createForm(CreateEmploymentDTO form, Staff hrStaff) { // Set default status to PENDING formEntity.setStatus(ApprovalStatus.PENDING); formEntity.setCreatedBy(hrStaff); + formEntity.setSocialSecurityNumber( + encryptionService.encrypt(form.getSocialSecurityNumber()) + ); - EmploymentFormDTO savedForm = employmentMapper.toDTO(employmentFormRepository.save(formEntity)); + EmploymentForm savedForm = employmentFormRepository.save(formEntity); logger.info("New employment form created with ID: {} by HR staffId={}", savedForm.getId(), hrStaff.getId()); - return savedForm; + return toSafeDto(savedForm); } @PreAuthorize("hasAnyRole('HR', 'CEO', 'CTO', 'ADMIN')") @@ -84,7 +90,7 @@ private List getAllForms() { // Get a single form by ID @PreAuthorize("hasAnyRole('HR', 'CEO', 'CTO', 'ADMIN')") public EmploymentFormDTO getFormById(Long formId) { - return employmentMapper.toDTO(findFormById(formId)); + return toSafeDto(findFormById(formId)); } // Update form before approval (only PENDING forms can be updated) @@ -110,18 +116,20 @@ public EmploymentFormDTO updateFormBeforeApproval(Long formId, UpdateEmploymentD && Objects.equals(existingForm.getCreatedBy().getId(), loggedInHr.getId()); if (!isAdmin && !isCreator) { - throw new IllegalStateException("Only the HR staff who created this form or an admin can update it"); + throw new AccessDeniedException("Only the HR staff who created this form or an admin can update it"); } - + String existingSsnPlain = encryptionService.decrypt(existingForm.getSocialSecurityNumber()); + String newSsnPlain = updatedForm.getSocialSecurityNumber(); // Check for duplicate SSN if it's changed - if (!existingForm.getSocialSecurityNumber().equals(updatedForm.getSocialSecurityNumber())) { - validateSsnNotExists(updatedForm.getSocialSecurityNumber()); + if (!existingSsnPlain.equals(newSsnPlain)) { + validateSsnNotExists(newSsnPlain); + existingForm.setSocialSecurityNumber(encryptionService.encrypt(newSsnPlain)); } employmentMapper.updateEntity(updatedForm, existingForm); logger.info("Form {} updated by staffId={}", formId, loggedInHr.getId()); - return employmentMapper.toDTO(employmentFormRepository.save(existingForm)); + return toSafeDto(employmentFormRepository.save(existingForm)); } // Reject a form (only PENDING forms can be rejected, and only by management) @@ -227,14 +235,34 @@ private String generateSecurePassword() { } private void validateSsnNotExists(String ssn) { - if (employmentFormRepository.existsBySocialSecurityNumber(ssn)) { - throw new IllegalStateException("An application with this SSN already exists."); + + // TODO: Replace with SSN hash lookup when database schema is updated + // Current workaround: encrypt before searching (requires double encryption in createForm which is costly) + // Check employment forms by decrypting and comparing + List existingForms = employmentFormRepository.findAll(); + for (EmploymentForm form : existingForms) { + String decryptedSsn = encryptionService.decrypt(form.getSocialSecurityNumber()); + if (decryptedSsn.equals(ssn)) { + throw new IllegalStateException("An application with this SSN already exists."); + } } - if (staffRepository.existsBySocialSecurityNumber(ssn)) { - throw new IllegalStateException("An employee with this SSN already exists."); + + // Check staff by decrypting and comparing + List existingStaff = staffRepository.findAll(); + for (Staff staff : existingStaff) { + String decryptedSsn = encryptionService.decrypt(staff.getSocialSecurityNumber()); + if (decryptedSsn.equals(ssn)) { + throw new IllegalStateException("An employee with this SSN already exists."); + } } } + private EmploymentFormDTO toSafeDto(EmploymentForm form) { + EmploymentFormDTO dto = employmentMapper.toDTO(form); + dto.setSocialSecurityNumber(encryptionService.maskLastFour(form.getSocialSecurityNumber())); + return dto; + } + private void archiveToS3(EmploymentForm form) { try { // Rewrite form-data to json diff --git a/src/main/java/org/example/cyberwatch/features/staff/controller/StaffRestController.java b/src/main/java/org/example/cyberwatch/features/staff/controller/StaffRestController.java index 2450e8a..ee9505a 100644 --- a/src/main/java/org/example/cyberwatch/features/staff/controller/StaffRestController.java +++ b/src/main/java/org/example/cyberwatch/features/staff/controller/StaffRestController.java @@ -24,11 +24,12 @@ public StaffRestController(StaffService staffService) { } @GetMapping("/me") + //skapa en ny metod i service public ResponseEntity getCurrentUser(@AuthenticationPrincipal Staff staff) { if (staff == null) { return ResponseEntity.status(401).build(); } - return ResponseEntity.ok(staffService.getStaffById(staff.getId())); + return ResponseEntity.ok(staffService.getUserStaff(staff)); } @PatchMapping("/me/status") @@ -41,17 +42,20 @@ public ResponseEntity updateMyStatus(@AuthenticationPrincipal Staff st if (!List.of("ONLINE", "BUSY", "AWAY", "OFFLINE").contains(status)) { return ResponseEntity.badRequest().build(); } - return ResponseEntity.ok(staffService.updateStatus(staff.getId(), status)); + return ResponseEntity.ok(staffService.updateStatus(staff.getId(), status, staff)); } @GetMapping("/{id}") - public ResponseEntity getStaffById(@PathVariable Long id) { - return ResponseEntity.ok(staffService.getStaffById(id)); + public ResponseEntity getStaffById(@PathVariable Long id, + @AuthenticationPrincipal Staff user) { + + return ResponseEntity.ok(staffService.getStaffById(id, user)); + } @PutMapping("/{id}") - public ResponseEntity updateStaff(@PathVariable Long id, @Valid @RequestBody UpdateStaffDTO dto) { - return ResponseEntity.ok(staffService.updateStaff(id, dto)); + public ResponseEntity updateStaff(@PathVariable Long id, @Valid @RequestBody UpdateStaffDTO dto, @AuthenticationPrincipal Staff staff) { + return ResponseEntity.ok(staffService.updateStaff(id, dto, staff)); } @DeleteMapping("/{id}") @@ -63,7 +67,8 @@ public ResponseEntity deleteStaff(@PathVariable Long id) { @GetMapping public ResponseEntity> getStaffByRoleOrDepartment( @RequestParam(required = false) Role role, - @RequestParam(required = false) Department department) { - return ResponseEntity.ok(staffService.getStaffByRoleOrDepartment(role, department)); + @RequestParam(required = false) Department department, + @AuthenticationPrincipal Staff user) { + return ResponseEntity.ok(staffService.getStaffByRoleOrDepartment(role, department, user)); } } \ No newline at end of file diff --git a/src/main/java/org/example/cyberwatch/features/staff/model/Staff.java b/src/main/java/org/example/cyberwatch/features/staff/model/Staff.java index 0eef5db..7a3d4e9 100644 --- a/src/main/java/org/example/cyberwatch/features/staff/model/Staff.java +++ b/src/main/java/org/example/cyberwatch/features/staff/model/Staff.java @@ -21,7 +21,6 @@ public class Staff { @Column(name = "social_security_number", nullable = false, unique = true) @NotBlank(message = "Social security number cannot be blank") - @Pattern(regexp = "\\d{6}-\\d{4}|\\d{8}-\\d{4}", message = "Social security number must be in format YYMMDD-NNNN or YYYYMMDD-NNNN") private String socialSecurityNumber; @Column(name = "first_name") diff --git a/src/main/java/org/example/cyberwatch/features/staff/service/StaffService.java b/src/main/java/org/example/cyberwatch/features/staff/service/StaffService.java index b3a874a..5783727 100644 --- a/src/main/java/org/example/cyberwatch/features/staff/service/StaffService.java +++ b/src/main/java/org/example/cyberwatch/features/staff/service/StaffService.java @@ -1,5 +1,6 @@ package org.example.cyberwatch.features.staff.service; +import org.example.cyberwatch.config.security.EncryptionService; import org.example.cyberwatch.features.staff.exception.StaffNotFoundException; import org.example.cyberwatch.features.staff.model.Staff; import org.example.cyberwatch.features.staff.model.StaffDTO; @@ -27,28 +28,33 @@ public class StaffService { private final StaffRepository staffRepository; private final StaffMapper staffMapper; + private final EncryptionService encryptionService; - public StaffService(StaffRepository staffRepository, StaffMapper staffMapper) { + public StaffService(StaffRepository staffRepository, StaffMapper staffMapper, EncryptionService encryptionService) { this.staffRepository = staffRepository; this.staffMapper = staffMapper; + this.encryptionService = encryptionService; } - public StaffDTO getStaffById(Long id) { + public StaffDTO getStaffById(Long id, Staff requester) { if (id == null) { throw new IllegalArgumentException("Staff ID cannot be null"); } - return staffMapper.toDto( - staffRepository.findById(id) - .orElseThrow(() -> new StaffNotFoundException("Staff not found with id: " + id)) - ); + Staff staff = staffRepository.findById(id) + .orElseThrow(() -> new StaffNotFoundException("Staff not found with id: " + id)); + + return toDtoWithSsnPolicy(staff, requester); } - private List getAllStaff() { - return staffMapper.toDTOList(staffRepository.findAll()); + public StaffDTO getUserStaff(Staff user) { + if (user == null) { + throw new IllegalArgumentException("Staff cannot be null"); + } + return toMaskedDto(user); } @PreAuthorize("hasAnyRole('HR', 'ADMIN')") - public StaffDTO updateStaff(Long staffId, UpdateStaffDTO dto) { + public StaffDTO updateStaff(Long staffId, UpdateStaffDTO dto, Staff requester) { if (staffId == null) { throw new IllegalArgumentException("Staff ID cannot be null"); } @@ -66,9 +72,10 @@ public StaffDTO updateStaff(Long staffId, UpdateStaffDTO dto) { } staffMapper.updateEntity(dto, existingStaff); + Staff savedStaff = staffRepository.save(existingStaff); logger.info("Staff {} updated", staffId); - return staffMapper.toDto(staffRepository.save(existingStaff)); + return toDtoWithSsnPolicy(savedStaff, requester); // Returnerar maskat SSN efter uppdatering } @PreAuthorize("hasAnyRole('HR', 'ADMIN')") @@ -82,22 +89,23 @@ public void deleteStaff(Long staffId) { logger.info("Staff {} deleted", staffId); } - public List getStaffByRoleOrDepartment(Role role, Department department) { + public List getStaffByRoleOrDepartment(Role role, Department department, Staff requester) { + + List staffList; if (role != null) { - return staffRepository.findByRole(role) - .stream() - .map(staffMapper::toDto) - .toList(); + staffList = staffRepository.findByRole(role); } else if (department != null) { - return staffRepository.findByDepartment(department) - .stream() - .map(staffMapper::toDto) - .toList(); + staffList = staffRepository.findByDepartment(department); + } else { + staffList = staffRepository.findAll(); } - return getAllStaff(); + + return staffList.stream() + .map(s -> toDtoWithSsnPolicy(s, requester)) + .toList(); } - public StaffDTO updateStatus(Long staffId, String status) { + public StaffDTO updateStatus(Long staffId, String status, Staff requester) { if (staffId == null) { throw new IllegalArgumentException("Staff ID cannot be null"); } @@ -110,8 +118,29 @@ public StaffDTO updateStatus(Long staffId, String status) { .orElseThrow(() -> new StaffNotFoundException("Staff not found with id: " + staffId)); staff.setStatus(status); + Staff savedStaff = staffRepository.save(staff); logger.info("Staff {} status updated to {}", staffId, status); - return staffMapper.toDto(staffRepository.save(staff)); + return toDtoWithSsnPolicy(savedStaff, requester); // Returnerar maskat SSN efter statusuppdatering + } + + + // ADMIN/HR får se hela, andra får se maskat. + private StaffDTO toDtoWithSsnPolicy(Staff staff, Staff requester) { + StaffDTO dto = staffMapper.toDto(staff); + String rawEncryptedSsn = staff.getSocialSecurityNumber(); + + if (requester != null && (requester.getRole() == Role.ADMIN || requester.getRole() == Role.HR)) { + dto.setSocialSecurityNumber(encryptionService.decrypt(rawEncryptedSsn)); + } else { + dto.setSocialSecurityNumber(encryptionService.maskLastFour(rawEncryptedSsn)); + } + return dto; + } + + private StaffDTO toMaskedDto(Staff staff) { + StaffDTO dto = staffMapper.toDto(staff); + dto.setSocialSecurityNumber(encryptionService.maskLastFour(staff.getSocialSecurityNumber())); + return dto; } } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index caa585a..405af20 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,24 +1,24 @@ spring.application.name=CyberWatch - -spring.datasource.url=${DATABASE_URL:jdbc:postgresql://localhost:5433/mydatabase} -spring.datasource.username=${DATABASE_USER:myuser} -spring.datasource.password=${DATABASE_PASSWORD:secret} +spring.datasource.url=${DATABASE_URL} +spring.datasource.username=${DATABASE_USER} +spring.datasource.password=${DATABASE_PASSWORD} spring.jpa.hibernate.ddl-auto=none spring.jpa.show-sql=true spring.flyway.enabled=true spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=10MB - -app.s3.endpoint=http://localhost:9002 +app.s3.endpoint=${S3_ENDPOINT} app.s3.region=eu-north-1 -app.s3.accessKey=${S3_ACCESS_KEY:minioadmin} -app.s3.secretKey=${S3_SECRET_KEY:minioadmin} +app.s3.accessKey=${S3_ACCESS_KEY} +app.s3.secretKey=${S3_SECRET_KEY} app.s3.bucket=ticket-files app.s3.allowed-origins=http://localhost:8080 spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID} spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET} spring.security.oauth2.client.registration.google.scope=email,profile +app.encryption.key=${ENCRYPTION_KEY} +app.encryption.salt=${ENCRYPTION_SALT} management.endpoints.web.exposure.include=logfile management.endpoint.logfile.enabled=true diff --git a/src/main/resources/db/migration/V6__add_test_user.sql b/src/main/resources/db/migration/V6__add_test_user.sql deleted file mode 100644 index baeac64..0000000 --- a/src/main/resources/db/migration/V6__add_test_user.sql +++ /dev/null @@ -1,7 +0,0 @@ -INSERT INTO staff (social_security_number, first_name, last_name, email, phone_number, role, department, password) -VALUES ('19900101-1234', 'Eric', 'Thilen', 'ericthilen2003@gmail.com', '0701234567', 'ADMIN', 'MANAGEMENT', '$2a$10$w5pG/fM/yT.x/jZ0j2/H/.8yK5PzX3Y/O5F7y/6C6e9zX6y/O5F7y') -ON CONFLICT (social_security_number) DO NOTHING; -- Avoid duplicate key errors if already present - -INSERT INTO staff (social_security_number, first_name, last_name, email, phone_number, role, department, password) -VALUES ('19900102-1234', 'Younes', 'Lamia', 'younescool94@gmail.com', '0707939910', 'ADMIN', 'MANAGEMENT', '$2a$12$zB3FM4WIZYqD5DIQDLg2BOje000ZPYc1EP9JRU1cNK2TDw7gyHQIm') - ON CONFLICT (social_security_number) DO NOTHING; -- Avoid duplicate key errors if already present diff --git a/src/main/resources/db/migration/V8__add_admin_user.sql b/src/main/resources/db/migration/V8__add_admin_user.sql deleted file mode 100644 index fdd49c0..0000000 --- a/src/main/resources/db/migration/V8__add_admin_user.sql +++ /dev/null @@ -1,4 +0,0 @@ -INSERT INTO staff (social_security_number, first_name, last_name, email, phone_number, role, department, password) -VALUES ('19980508-3872', 'Caroline', 'Nordbrandt', 'nordbrandtcaroline@gmail.com', '0936458263', 'ADMIN', 'MANAGEMENT', - '$2a$12$DfSHWL9xvGe/WSRNVpAww.FCn0oJ4TmNyIXbLgtqIsl.k3D9MraQ2') -ON CONFLICT (social_security_number) DO NOTHING; \ No newline at end of file diff --git a/src/test/java/org/example/cyberwatch/SecurityIntegrationTests.java b/src/test/java/org/example/cyberwatch/SecurityIntegrationTests.java index aed29ea..f1b0945 100644 --- a/src/test/java/org/example/cyberwatch/SecurityIntegrationTests.java +++ b/src/test/java/org/example/cyberwatch/SecurityIntegrationTests.java @@ -1,5 +1,7 @@ package org.example.cyberwatch; +import org.example.cyberwatch.features.staff.model.Staff; +import org.example.cyberwatch.shared.model.enums.Role; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -7,9 +9,15 @@ import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import java.util.List; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; @@ -17,6 +25,7 @@ @SpringBootTest @Import(TestcontainersConfiguration.class) +@ActiveProfiles("test") @AutoConfigureMockMvc class SecurityIntegrationTests { @@ -25,21 +34,25 @@ class SecurityIntegrationTests { // ── Staff ────────────────────────────────────────── - /* - @Test - @WithMockUser(roles = "CONSULTANT") - @DisplayName("Consultant should not access staff endpoints") - void consultantCannotAccessStaff() throws Exception { - mockMvc.perform(get("/api/staff")) - .andExpect(status().isForbidden()); - } Kommer ändras senare - */ - @Test - @WithMockUser(roles = "HR") @DisplayName("HR should be able to access staff endpoints") void hrCanAccessStaff() throws Exception { - mockMvc.perform(get("/api/staff")) + Staff hrUser = new Staff(); + hrUser.setFirstName("Test"); + hrUser.setLastName("HR"); + hrUser.setEmail("hr@cyberwatch.com"); + hrUser.setRole(Role.HR); + + // Skapa en Authentication-token. + // hrUser som "principal" (identitet). + var auth = new UsernamePasswordAuthenticationToken( + hrUser, // Principal (det som hamnar i @AuthenticationPrincipal) + null, // Credentials (behövs inte här) + List.of(new SimpleGrantedAuthority("ROLE_HR")) // Rollen för filter-säkerheten + ); + + mockMvc.perform(get("/api/staff") + .with(authentication(auth))) .andExpect(status().isOk()); } @@ -73,11 +86,24 @@ void hrCanCreateForm() throws Exception { } @Test - @WithMockUser(roles = "HR") @DisplayName("HR should not be able to approve forms") void hrCannotApproveForm() throws Exception { + Staff hrUser = new Staff(); + hrUser.setId(10L); + hrUser.setEmail("hr@cyberwatch.local"); + hrUser.setRole(Role.HR); + + //Skapa en Authentication-token där din Staff är "Principal" + var auth = new UsernamePasswordAuthenticationToken( + hrUser, + null, + List.of(new SimpleGrantedAuthority("ROLE_HR")) + ); + + // 3. Skicka med denna auth i perform-anropet mockMvc.perform(post("/api/forms/1/approve") - .with(csrf())) + .with(csrf()) + .with(authentication(auth))) //Staff som principal .andDo(print()) .andExpect(status().isForbidden()); } diff --git a/src/test/java/org/example/cyberwatch/TicketAttachmentSecurityTest.java b/src/test/java/org/example/cyberwatch/TicketAttachmentSecurityTest.java index fd90b21..52ee530 100644 --- a/src/test/java/org/example/cyberwatch/TicketAttachmentSecurityTest.java +++ b/src/test/java/org/example/cyberwatch/TicketAttachmentSecurityTest.java @@ -8,13 +8,15 @@ import org.springframework.context.annotation.Import; import org.springframework.mock.web.MockMultipartFile; import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.presigner.S3Presigner; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** @@ -30,6 +32,7 @@ */ @SpringBootTest @Import(TestcontainersConfiguration.class) +@ActiveProfiles("test") @AutoConfigureMockMvc class TicketAttachmentSecurityTest { diff --git a/src/test/java/org/example/cyberwatch/features/form/service/EmploymentFormServiceTest.java b/src/test/java/org/example/cyberwatch/features/form/service/EmploymentFormServiceTest.java index d614396..b8601b0 100644 --- a/src/test/java/org/example/cyberwatch/features/form/service/EmploymentFormServiceTest.java +++ b/src/test/java/org/example/cyberwatch/features/form/service/EmploymentFormServiceTest.java @@ -1,5 +1,6 @@ package org.example.cyberwatch.features.form.service; +import org.example.cyberwatch.config.security.EncryptionService; import org.example.cyberwatch.features.form.dto.CreateEmploymentDTO; import org.example.cyberwatch.features.form.dto.EmploymentFormDTO; import org.example.cyberwatch.features.form.dto.UpdateEmploymentDTO; @@ -19,9 +20,11 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.access.AccessDeniedException; import org.springframework.security.crypto.password.PasswordEncoder; import tools.jackson.databind.ObjectMapper; +import java.util.List; import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; @@ -43,51 +46,67 @@ class EmploymentFormServiceTest { private ObjectMapper objectMapper; @Mock private PasswordEncoder passwordEncoder; + @Mock + private EncryptionService encryptionService; // ny @InjectMocks private EmploymentFormService service; + // Hjälpmetod för att slippa upprepa + private Staff createHrStaff() { + Staff staff = new Staff(); + staff.setEmail("hr@cyberwatch.local"); + staff.setRole(Role.HR); + return staff; + } + @Test - @DisplayName("Should successfully create an employment form") + @DisplayName("Should successfully create form and ensure SSN is encrypted before saving") void createForm_Success() { // Arrange - CreateEmploymentDTO dto = new CreateEmploymentDTO("19900101-1234", "Alice", "Andersson", "alice@test.com", "070", Role.HR, Department.BACKEND, null, null, null); - Staff hrStaff = new Staff(); - hrStaff.setEmail("hr@cyberwatch.local"); + String rawSsn = "19900101-1234"; + String encryptedSsn = "krypterat-ssn"; + CreateEmploymentDTO dto = new CreateEmploymentDTO( + rawSsn, "Alice", "Andersson", + "alice@test.com", "070", Role.HR, Department.BACKEND, null, null, null + ); + Staff hrStaff = createHrStaff(); EmploymentForm entity = new EmploymentForm(); - EmploymentFormDTO expectedDto = new EmploymentFormDTO(); - expectedDto.setSocialSecurityNumber("19900101-1234"); - when(formRepository.existsBySocialSecurityNumber(anyString())).thenReturn(false); - when(staffRepository.existsBySocialSecurityNumber(anyString())).thenReturn(false); + when(formRepository.findAll()).thenReturn(List.of()); + when(staffRepository.findAll()).thenReturn(List.of()); + when(encryptionService.encrypt(rawSsn)).thenReturn(encryptedSsn); when(mapper.toEntity(dto)).thenReturn(entity); when(formRepository.save(any(EmploymentForm.class))).thenReturn(entity); - when(mapper.toDTO(entity)).thenReturn(expectedDto); + when(mapper.toDTO(entity)).thenReturn(new EmploymentFormDTO()); // Act EmploymentFormDTO result = service.createForm(dto, hrStaff); // Assert assertNotNull(result); - assertEquals("19900101-1234", result.getSocialSecurityNumber()); - verify(formRepository, times(1)).save(any()); + verify(encryptionService).encrypt(rawSsn); + assertEquals(encryptedSsn, entity.getSocialSecurityNumber(), + "The entity should hold the encrypted SSN when saved"); + + verify(formRepository).save(entity); } + @Test @DisplayName("Should throw exception if user is not creator or admin") void updateForm_UnauthorizedUser_ThrowsException() { + // Arrange Long formId = 1L; - UpdateEmploymentDTO updateDto = new UpdateEmploymentDTO(); - Staff creator = new Staff(); creator.setId(1L); creator.setEmail("owner@cyberwatch.local"); Staff otherHr = new Staff(); - otherHr.setId(2L); otherHr.setEmail("other@cyberwatch.local"); - otherHr.setRole(Role.HR); // inte admin, inte creator + otherHr.setId(2L); + otherHr.setRole(Role.HR); EmploymentForm existingForm = new EmploymentForm(); existingForm.setCreatedBy(creator); @@ -95,8 +114,9 @@ void updateForm_UnauthorizedUser_ThrowsException() { when(formRepository.findById(formId)).thenReturn(Optional.of(existingForm)); - assertThrows(IllegalStateException.class, () -> - service.updateFormBeforeApproval(formId, updateDto, otherHr) + // Act & Assert + assertThrows(AccessDeniedException.class, () -> + service.updateFormBeforeApproval(formId, new UpdateEmploymentDTO(), otherHr) ); verify(formRepository, never()).save(any()); } @@ -112,6 +132,8 @@ void rejectForm_Success() { cto.setRole(Role.CTO); when(formRepository.findById(id)).thenReturn(Optional.of(form)); + when(mapper.toDTO(form)).thenReturn(new EmploymentFormDTO()); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); // Act service.rejectForm(id, cto); @@ -127,7 +149,7 @@ void approveAndFinalize_Success() throws Exception { // Arrange Long id = 1L; EmploymentForm form = new EmploymentForm(); - form.setSocialSecurityNumber("1990-1234"); + form.setSocialSecurityNumber("krypterat-ssn"); form.setStatus(ApprovalStatus.PENDING); Staff cto = new Staff(); @@ -137,7 +159,6 @@ void approveAndFinalize_Success() throws Exception { when(formRepository.findById(id)).thenReturn(Optional.of(form)); when(mapper.formToStaff(form)).thenReturn(newEmployee); when(passwordEncoder.encode(anyString())).thenReturn("hashed_pass"); - when(mapper.toDTO(form)).thenReturn(new EmploymentFormDTO()); when(objectMapper.writeValueAsString(any())).thenReturn("{}"); @@ -154,18 +175,20 @@ void approveAndFinalize_Success() throws Exception { @Test @DisplayName("Should throw exception if SSN already exists in form repository") void createForm_DuplicateSsnInForms_ThrowsException() { - //arrange + // Arrange Staff creator = new Staff(); creator.setEmail("owner@cyberwatch.local"); CreateEmploymentDTO dto = new CreateEmploymentDTO( "19900101-1234", "Alice", "Andersson", "alice@test.com", "070", Role.HR, Department.BACKEND, null, null, null ); + EmploymentForm existing = new EmploymentForm(); + existing.setSocialSecurityNumber("krypterat-ssn"); - //act - when(formRepository.existsBySocialSecurityNumber("19900101-1234")).thenReturn(true); + when(formRepository.findAll()).thenReturn(List.of(existing)); + when(encryptionService.decrypt("krypterat-ssn")).thenReturn("19900101-1234"); - //assert + // Act & Assert assertThrows(IllegalStateException.class, () -> service.createForm(dto, creator) ); @@ -175,28 +198,34 @@ void createForm_DuplicateSsnInForms_ThrowsException() { @Test @DisplayName("Should throw exception if SSN already exists in staff repository") void createForm_DuplicateSsnInStaff_ThrowsException() { - //arrange + // Arrange Staff creator = new Staff(); - creator.setEmail("owner@cyberwatch.local"); + String rawSsn = "19900101-1234"; + CreateEmploymentDTO dto = new CreateEmploymentDTO( - "19900101-1234", "Alice", "Andersson", + rawSsn, "Alice", "Andersson", "alice@test.com", "070", Role.HR, Department.BACKEND, null, null, null ); - //act - when(staffRepository.existsBySocialSecurityNumber("19900101-1234")).thenReturn(true); + Staff existingStaff = new Staff(); + existingStaff.setSocialSecurityNumber("krypterat-ssn"); - //assert + when(formRepository.findAll()).thenReturn(List.of());//no match + when(staffRepository.findAll()).thenReturn(List.of(existingStaff)); + when(encryptionService.decrypt("krypterat-ssn")).thenReturn("19900101-1234"); + + // Act & Assert assertThrows(IllegalStateException.class, () -> service.createForm(dto, creator) ); + verify(formRepository, never()).save(any()); } @Test @DisplayName("Should throw exception if form is not PENDING when updating") void updateForm_NotPendingStatus_ThrowsException() { - //arrange + // Arrange Staff updater = new Staff(); updater.setEmail("owner@cyberwatch.local"); Long formId = 1L; @@ -204,10 +233,9 @@ void updateForm_NotPendingStatus_ThrowsException() { EmploymentForm existingForm = new EmploymentForm(); existingForm.setStatus(ApprovalStatus.APPROVED); - //act when(formRepository.findById(formId)).thenReturn(Optional.of(existingForm)); - //assert + // Act & Assert assertThrows(IllegalStateException.class, () -> service.updateFormBeforeApproval(formId, updateDto, updater) ); @@ -217,19 +245,18 @@ void updateForm_NotPendingStatus_ThrowsException() { @Test @DisplayName("Should throw exception if form is not PENDING when rejecting") void rejectForm_NotPendingStatus_ThrowsException() { - //arrange - Staff rejecter = new Staff(); - rejecter.setEmail("owner@cyberwatch.local"); + // Arrange Long formId = 1L; EmploymentForm form = new EmploymentForm(); form.setStatus(ApprovalStatus.APPROVED); + Staff cto = new Staff(); + cto.setRole(Role.CTO); - //act when(formRepository.findById(formId)).thenReturn(Optional.of(form)); - //assert + // Act & Assert assertThrows(IllegalStateException.class, () -> - service.rejectForm(formId, rejecter) + service.rejectForm(formId, cto) ); verify(formRepository, never()).save(any()); } @@ -237,30 +264,31 @@ void rejectForm_NotPendingStatus_ThrowsException() { @Test @DisplayName("Should throw exception if form not found") void rejectForm_FormNotFound_ThrowsException() { - Staff rejecter = new Staff(); - rejecter.setEmail("owner@cyberwatch.local"); + // Arrange + Staff cto = new Staff(); + cto.setRole(Role.CTO); + + // Act & Assert assertThrows(EmploymentFormNotFound.class, () -> - service.rejectForm(99L, rejecter) + service.rejectForm(99L, cto) ); } @Test @DisplayName("Should throw exception if form is not PENDING when approving") void approveAndFinalize_NotPendingStatus_ThrowsException() { - //arrange - Staff approver = new Staff(); - approver.setEmail("owner@cyberwatch.local"); - approver.setRole(Role.CTO); + // Arrange Long formId = 1L; EmploymentForm form = new EmploymentForm(); form.setStatus(ApprovalStatus.APPROVED); + Staff cto = new Staff(); + cto.setRole(Role.CTO); - //act when(formRepository.findById(formId)).thenReturn(Optional.of(form)); - //assert + // Act & Assert assertThrows(IllegalStateException.class, () -> - service.approveAndFinalizeEmployment(formId, approver) + service.approveAndFinalizeEmployment(formId, cto) ); verify(staffRepository, never()).save(any()); } @@ -268,18 +296,16 @@ void approveAndFinalize_NotPendingStatus_ThrowsException() { @Test @DisplayName("Should throw exception if approver is not CEO, CTO or ADMIN") void approveAndFinalize_WrongRole_ThrowsException() { - //arrange + // Arrange Long formId = 1L; EmploymentForm form = new EmploymentForm(); form.setStatus(ApprovalStatus.PENDING); - Staff hr = new Staff(); hr.setRole(Role.HR); - //act when(formRepository.findById(formId)).thenReturn(Optional.of(form)); - //assert + // Act & Assert assertThrows(IllegalStateException.class, () -> service.approveAndFinalizeEmployment(formId, hr) ); diff --git a/src/test/java/org/example/cyberwatch/features/staff/service/StaffServiceTest.java b/src/test/java/org/example/cyberwatch/features/staff/service/StaffServiceTest.java index aa636f7..d3a3cc7 100644 --- a/src/test/java/org/example/cyberwatch/features/staff/service/StaffServiceTest.java +++ b/src/test/java/org/example/cyberwatch/features/staff/service/StaffServiceTest.java @@ -1,5 +1,6 @@ package org.example.cyberwatch.features.staff.service; +import org.example.cyberwatch.config.security.EncryptionService; import org.example.cyberwatch.features.staff.exception.StaffNotFoundException; import org.example.cyberwatch.features.staff.model.Staff; import org.example.cyberwatch.features.staff.model.StaffDTO; @@ -33,6 +34,8 @@ class StaffServiceTest { private StaffRepository staffRepository; @Mock private StaffMapper staffMapper; + @Mock + private EncryptionService encryptionService; @InjectMocks private StaffService staffService; @@ -50,6 +53,7 @@ void setUp() { newStaff.setEmail("anna@cyberwatch.se"); newStaff.setRole(Role.HR); newStaff.setDepartment(Department.BACKEND); + newStaff.setSocialSecurityNumber("krypterat-ssn"); newStaffDTO = new StaffDTO(1L, null, "Anna", "Svensson", "anna@cyberwatch.se", null, Role.HR, Department.BACKEND, "profil.png", "ONLINE"); @@ -58,6 +62,7 @@ void setUp() { @Test void updateStatus_ShouldUpdateAndReturnStaffDTO() { + newStaff.setRole(Role.HR); Long staffId = 1L; String status = "BUSY"; Staff staff = new Staff(); @@ -72,43 +77,62 @@ void updateStatus_ShouldUpdateAndReturnStaffDTO() { when(staffRepository.save(any(Staff.class))).thenReturn(staff); when(staffMapper.toDto(any(Staff.class))).thenReturn(staffDTO); - StaffDTO result = staffService.updateStatus(staffId, status); + StaffDTO result = staffService.updateStatus(staffId, status, newStaff); assertEquals(status, result.getStatus()); assertEquals(status, staff.getStatus()); } @Test - @DisplayName("Should return DTO when staff exists") - void getStaffByIdStaffExists() { + @DisplayName("Should return masked SSN for non-admin") + void getStaffById_NonAdminOrHr_ReturnsMaskedSsn() { + newStaff.setRole(Role.CONSULTANT); + when(staffRepository.findById(1L)).thenReturn(Optional.of(newStaff)); + when(staffMapper.toDto(newStaff)).thenReturn(newStaffDTO); + when(encryptionService.maskLastFour("krypterat-ssn")).thenReturn("19900101-****"); + + StaffDTO result = staffService.getStaffById(1L, newStaff); + + assertThat(result.getSocialSecurityNumber()).isEqualTo("19900101-****"); + verify(encryptionService).maskLastFour("krypterat-ssn"); + verify(encryptionService, never()).decrypt(any()); + } + + @Test + @DisplayName("Should return decrypted SSN for admin") + void getStaffById_Admin_ReturnsDecryptedSsn() { + newStaff.setRole(Role.ADMIN); when(staffRepository.findById(1L)).thenReturn(Optional.of(newStaff)); when(staffMapper.toDto(newStaff)).thenReturn(newStaffDTO); + when(encryptionService.decrypt("krypterat-ssn")).thenReturn("19900101-1234"); - StaffDTO result = staffService.getStaffById(1L); + StaffDTO result = staffService.getStaffById(1L, newStaff); - assertThat(result.getId()).isEqualTo(1L); - assertThat(result.getEmail()).isEqualTo("anna@cyberwatch.se"); + assertThat(result.getSocialSecurityNumber()).isEqualTo("19900101-1234"); + verify(encryptionService).decrypt("krypterat-ssn"); + verify(encryptionService, never()).maskLastFour(any()); } @Test @DisplayName("Should throw StaffNotFoundException when staff not found") - void getStaffByIdNotFound() { + void getStaffById_NotFound() { when(staffRepository.findById(99L)).thenReturn(Optional.empty()); - assertThatThrownBy(() -> staffService.getStaffById(99L)) + assertThatThrownBy(() -> staffService.getStaffById(99L, newStaff)) .isInstanceOf(StaffNotFoundException.class); } @Test @DisplayName("Should throw IllegalArgumentException when id is null") - void getStaffByIdIsNull() { - assertThatThrownBy(() -> staffService.getStaffById(null)) + void getStaffById_NullId() { + assertThatThrownBy(() -> staffService.getStaffById(null, newStaff)) .isInstanceOf(IllegalArgumentException.class); } @Test @DisplayName("Should update and return DTO when valid input") - void updateStaffWhenValid() { + void updateStaff_Valid() { + newStaff.setRole(Role.ADMIN); UpdateStaffDTO dto = new UpdateStaffDTO("Anna", "Nilsson", "anna@cyberwatch.se", "0701234567", Role.HR, Department.BACKEND); @@ -116,7 +140,7 @@ void updateStaffWhenValid() { when(staffRepository.save(newStaff)).thenReturn(newStaff); when(staffMapper.toDto(newStaff)).thenReturn(newStaffDTO); - StaffDTO result = staffService.updateStaff(1L, dto); + StaffDTO result = staffService.updateStaff(1L, dto, newStaff); verify(staffMapper).updateEntity(dto, newStaff); verify(staffRepository).save(newStaff); @@ -125,11 +149,11 @@ void updateStaffWhenValid() { @Test @DisplayName("Should throw StaffNotFoundException when updating non-existent staff") - void updateStaffNotFound() { - UpdateStaffDTO dto = new UpdateStaffDTO(); + void updateStaff_NotFound() { + newStaff.setRole(Role.ADMIN); when(staffRepository.findById(99L)).thenReturn(Optional.empty()); - assertThatThrownBy(() -> staffService.updateStaff(99L, dto)) + assertThatThrownBy(() -> staffService.updateStaff(99L, new UpdateStaffDTO(), newStaff)) .isInstanceOf(StaffNotFoundException.class); } @@ -145,7 +169,7 @@ void deleteStaffExists() { @Test @DisplayName("Should throw StaffNotFoundException when deleting non-existent staff") - void deleteStaffNotFound() { + void deleteStaff_NotFound() { when(staffRepository.findById(99L)).thenReturn(Optional.empty()); assertThatThrownBy(() -> staffService.deleteStaff(99L)) @@ -153,12 +177,14 @@ void deleteStaffNotFound() { } @Test - @DisplayName("Should filter by role when role is provided") - void getStaffByRoleOrDepartmentFilteredByRole() { + @DisplayName("Should filter by role and mask SSN for non-admin or hr") + void getStaffByRoleOrDepartment_FilterByRole_MaskedSsn() { + newStaff.setRole(Role.CEO); when(staffRepository.findByRole(Role.HR)).thenReturn(List.of(newStaff)); when(staffMapper.toDto(newStaff)).thenReturn(newStaffDTO); + when(encryptionService.maskLastFour("krypterat-ssn")).thenReturn("19900101-****"); - List result = staffService.getStaffByRoleOrDepartment(Role.HR, null); + List result = staffService.getStaffByRoleOrDepartment(Role.HR, null, newStaff); assertThat(result).hasSize(1); verify(staffRepository, times(1)).findByRole(Role.HR); @@ -166,12 +192,24 @@ void getStaffByRoleOrDepartmentFilteredByRole() { } @Test - @DisplayName("Should filter by department when department is provided") - void getStaffByRoleOrDepartmentFilteredByDepartment() { - when(staffRepository.findByDepartment(Department.BACKEND)).thenReturn(List.of(newStaff)); + @DisplayName("Should filter by role and decrypt SSN for admin") + void getStaffByRoleOrDepartment_FilterByRole_DecryptedSsn() { + when(staffRepository.findByRole(Role.HR)).thenReturn(List.of(newStaff)); when(staffMapper.toDto(newStaff)).thenReturn(newStaffDTO); + when(encryptionService.decrypt("krypterat-ssn")).thenReturn("19900101-1234"); - List result = staffService.getStaffByRoleOrDepartment(null, Department.BACKEND); + List result = staffService.getStaffByRoleOrDepartment(Role.HR, null, newStaff); + + assertThat(result).hasSize(1); + assertThat(result.getFirst().getSocialSecurityNumber()).isEqualTo("19900101-1234"); + } + + @Test + @DisplayName("Should filter by department") + void getStaffByRoleOrDepartment_FilterByDepartment() { + when(staffRepository.findByDepartment(Department.BACKEND)).thenReturn(List.of(newStaff)); + when(staffMapper.toDto(newStaff)).thenReturn(newStaffDTO); + List result = staffService.getStaffByRoleOrDepartment(null, Department.BACKEND, newStaff); assertThat(result).hasSize(1); verify(staffRepository).findByDepartment(Department.BACKEND); @@ -185,12 +223,25 @@ void shouldHaveDefaultOfflineStatus() { } @Test - @DisplayName("Should return all staff when no filter is provided") - void getStaffByRoleOrDepartment() { + @DisplayName("Should return all staff when no filter") + void getStaffByRoleOrDepartment_NoFilter() { when(staffRepository.findAll()).thenReturn(List.of(newStaff)); - when(staffMapper.toDTOList(any())).thenReturn(List.of(newStaffDTO)); + when(staffMapper.toDto(newStaff)).thenReturn(newStaffDTO); + + List result = staffService.getStaffByRoleOrDepartment(null, null, newStaff); - List result = staffService.getStaffByRoleOrDepartment(null, null); + assertThat(result).hasSize(1); + verify(staffRepository).findAll(); + } + + @Test + @DisplayName("Should return all staff when no filter with masked last four") + void getStaffByRoleOrDepartment_NoFilter_MaskedFour() { + newStaff.setRole(Role.PROJECT_MANAGER); + when(staffRepository.findAll()).thenReturn(List.of(newStaff)); + when(staffMapper.toDto(newStaff)).thenReturn(newStaffDTO); + when(encryptionService.maskLastFour("krypterat-ssn")).thenReturn("19900101-****"); + List result = staffService.getStaffByRoleOrDepartment(null, null, newStaff); assertThat(result).hasSize(1); verify(staffRepository).findAll(); diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties new file mode 100644 index 0000000..47e7380 --- /dev/null +++ b/src/test/resources/application-test.properties @@ -0,0 +1,6 @@ +ENCRYPTION_KEY=testencryptionkey1234567890123456 +ENCRYPTION_SALT=0123456789abcdef +S3_ENDPOINT=http://localhost:9002 +S3_BUCKET=test-bucket +S3_ACCESS_KEY=minioadmin +S3_SECRET_KEY=minioadmin \ No newline at end of file