diff --git a/src/main/java/org/example/projektarendehantering/application/service/CaseNoteMapper.java b/src/main/java/org/example/projektarendehantering/application/service/CaseNoteMapper.java index a6a8f41..affb3e3 100644 --- a/src/main/java/org/example/projektarendehantering/application/service/CaseNoteMapper.java +++ b/src/main/java/org/example/projektarendehantering/application/service/CaseNoteMapper.java @@ -12,7 +12,9 @@ public CaseNoteDTO toDTO(CaseNoteEntity entity) { return new CaseNoteDTO( entity.getId(), entity.getContent(), - entity.getAuthor(), + entity.getAuthorDisplayName(), + entity.getAuthorGithubUsername(), + entity.getAuthorRole(), entity.getCreatedAt() ); } @@ -22,7 +24,9 @@ public CaseNoteEntity toEntity(CaseNoteDTO dto) { CaseNoteEntity entity = new CaseNoteEntity(); entity.setId(dto.getId()); entity.setContent(dto.getContent()); - entity.setAuthor(dto.getAuthor()); + entity.setAuthorDisplayName(dto.getAuthorDisplayName()); + entity.setAuthorGithubUsername(dto.getAuthorGithubUsername()); + entity.setAuthorRole(dto.getAuthorRole()); entity.setCreatedAt(dto.getCreatedAt()); return entity; } diff --git a/src/main/java/org/example/projektarendehantering/application/service/CaseService.java b/src/main/java/org/example/projektarendehantering/application/service/CaseService.java index 9482c7c..e5f443c 100644 --- a/src/main/java/org/example/projektarendehantering/application/service/CaseService.java +++ b/src/main/java/org/example/projektarendehantering/application/service/CaseService.java @@ -45,14 +45,18 @@ public CaseService(CaseRepository caseRepository, CaseMapper caseMapper, Patient } @Transactional - public void addNote(UUID caseId, String content, String author) { + public void addNote(UUID caseId, String content, Actor actor) { CaseEntity caseEntity = caseRepository.findById(caseId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Case not found")); CaseNoteEntity note = new CaseNoteEntity(); note.setCaseEntity(caseEntity); note.setContent(content); - note.setAuthor(author); + if (actor != null) { + note.setAuthorDisplayName(actor.displayName()); + note.setAuthorGithubUsername(actor.githubUsername()); + note.setAuthorRole(actor.role() != null ? actor.role().name() : null); + } note.setCreatedAt(Instant.now()); caseNoteRepository.save(note); diff --git a/src/main/java/org/example/projektarendehantering/application/service/EmployeeMapper.java b/src/main/java/org/example/projektarendehantering/application/service/EmployeeMapper.java index 5e43087..2df4c2f 100644 --- a/src/main/java/org/example/projektarendehantering/application/service/EmployeeMapper.java +++ b/src/main/java/org/example/projektarendehantering/application/service/EmployeeMapper.java @@ -5,6 +5,9 @@ import org.example.projektarendehantering.presentation.dto.EmployeeDTO; import org.springframework.stereotype.Component; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + @Component public class EmployeeMapper { @@ -13,6 +16,7 @@ public EmployeeDTO toDTO(EmployeeEntity entity) { return new EmployeeDTO( entity.getId(), entity.getDisplayName(), + entity.getGithubUsername(), entity.getRole(), entity.getCreatedAt() ); @@ -22,7 +26,13 @@ public EmployeeEntity toEntity(EmployeeCreateDTO dto) { if (dto == null) return null; EmployeeEntity entity = new EmployeeEntity(); entity.setDisplayName(dto.getDisplayName()); + entity.setGithubUsername(dto.getGithubUsername()); entity.setRole(dto.getRole()); + + if (dto.getGithubUsername() != null && !dto.getGithubUsername().isBlank()) { + UUID id = UUID.nameUUIDFromBytes(dto.getGithubUsername().getBytes(StandardCharsets.UTF_8)); + entity.setId(id); + } return entity; } } diff --git a/src/main/java/org/example/projektarendehantering/application/service/EmployeeService.java b/src/main/java/org/example/projektarendehantering/application/service/EmployeeService.java index 8304dde..6d4b4c6 100644 --- a/src/main/java/org/example/projektarendehantering/application/service/EmployeeService.java +++ b/src/main/java/org/example/projektarendehantering/application/service/EmployeeService.java @@ -1,6 +1,7 @@ package org.example.projektarendehantering.application.service; import org.example.projektarendehantering.common.Actor; +import org.example.projektarendehantering.common.BadRequestException; import org.example.projektarendehantering.common.NotAuthorizedException; import org.example.projektarendehantering.common.Role; import org.example.projektarendehantering.infrastructure.persistence.EmployeeEntity; @@ -30,8 +31,15 @@ public EmployeeService(EmployeeRepository employeeRepository, EmployeeMapper emp @Transactional public EmployeeDTO createEmployee(Actor actor, EmployeeCreateDTO dto) { requireCanManageEmployees(actor); + + if (employeeRepository.findByGithubUsername(dto.getGithubUsername()).isPresent()) { + throw new BadRequestException("EMPLOYEE_EXISTS", "Employee with username " + dto.getGithubUsername() + " already exists"); + } + EmployeeEntity entity = employeeMapper.toEntity(dto); - entity.setId(UUID.randomUUID()); + if (entity.getId() == null) { + entity.setId(UUID.randomUUID()); + } entity.setCreatedAt(Instant.now()); return employeeMapper.toDTO(employeeRepository.save(entity)); } diff --git a/src/main/java/org/example/projektarendehantering/common/Actor.java b/src/main/java/org/example/projektarendehantering/common/Actor.java index 1b701e2..a7ea168 100644 --- a/src/main/java/org/example/projektarendehantering/common/Actor.java +++ b/src/main/java/org/example/projektarendehantering/common/Actor.java @@ -2,7 +2,7 @@ import java.util.UUID; -public record Actor(UUID userId, Role role) { +public record Actor(UUID userId, Role role, String displayName, String githubUsername) { } diff --git a/src/main/java/org/example/projektarendehantering/infrastructure/config/SecurityConfig.java b/src/main/java/org/example/projektarendehantering/infrastructure/config/SecurityConfig.java index ca77fca..9b227de 100644 --- a/src/main/java/org/example/projektarendehantering/infrastructure/config/SecurityConfig.java +++ b/src/main/java/org/example/projektarendehantering/infrastructure/config/SecurityConfig.java @@ -2,6 +2,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.userdetails.User; @@ -12,6 +13,7 @@ @Configuration @EnableWebSecurity +@EnableMethodSecurity public class SecurityConfig { @Bean diff --git a/src/main/java/org/example/projektarendehantering/infrastructure/persistence/CaseNoteEntity.java b/src/main/java/org/example/projektarendehantering/infrastructure/persistence/CaseNoteEntity.java index 1eac7a2..f23fab6 100644 --- a/src/main/java/org/example/projektarendehantering/infrastructure/persistence/CaseNoteEntity.java +++ b/src/main/java/org/example/projektarendehantering/infrastructure/persistence/CaseNoteEntity.java @@ -27,7 +27,9 @@ public class CaseNoteEntity { private String content; - private String author; + private String authorDisplayName; + private String authorGithubUsername; + private String authorRole; private Instant createdAt; diff --git a/src/main/java/org/example/projektarendehantering/infrastructure/persistence/EmployeeEntity.java b/src/main/java/org/example/projektarendehantering/infrastructure/persistence/EmployeeEntity.java index 2c87e48..1e46f99 100644 --- a/src/main/java/org/example/projektarendehantering/infrastructure/persistence/EmployeeEntity.java +++ b/src/main/java/org/example/projektarendehantering/infrastructure/persistence/EmployeeEntity.java @@ -1,5 +1,6 @@ package org.example.projektarendehantering.infrastructure.persistence; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -19,6 +20,9 @@ public class EmployeeEntity { private String displayName; + @Column(unique = true) + private String githubUsername; + @Enumerated(EnumType.STRING) private Role role; @@ -26,9 +30,10 @@ public class EmployeeEntity { public EmployeeEntity() {} - public EmployeeEntity(UUID id, String displayName, Role role, Instant createdAt) { + public EmployeeEntity(UUID id, String displayName, String githubUsername, Role role, Instant createdAt) { this.id = id; this.displayName = displayName; + this.githubUsername = githubUsername; this.role = role; this.createdAt = createdAt; } @@ -39,6 +44,9 @@ public EmployeeEntity(UUID id, String displayName, Role role, Instant createdAt) public String getDisplayName() { return displayName; } public void setDisplayName(String displayName) { this.displayName = displayName; } + public String getGithubUsername() { return githubUsername; } + public void setGithubUsername(String githubUsername) { this.githubUsername = githubUsername; } + public Role getRole() { return role; } public void setRole(Role role) { this.role = role; } diff --git a/src/main/java/org/example/projektarendehantering/infrastructure/persistence/EmployeeRepository.java b/src/main/java/org/example/projektarendehantering/infrastructure/persistence/EmployeeRepository.java index 3e23e9d..081233b 100644 --- a/src/main/java/org/example/projektarendehantering/infrastructure/persistence/EmployeeRepository.java +++ b/src/main/java/org/example/projektarendehantering/infrastructure/persistence/EmployeeRepository.java @@ -2,8 +2,10 @@ import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; import java.util.UUID; public interface EmployeeRepository extends JpaRepository { + Optional findByGithubUsername(String githubUsername); } diff --git a/src/main/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapter.java b/src/main/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapter.java index fb8d7da..eda381a 100644 --- a/src/main/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapter.java +++ b/src/main/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapter.java @@ -3,8 +3,10 @@ import org.example.projektarendehantering.common.Actor; import org.example.projektarendehantering.common.NotAuthorizedException; import org.example.projektarendehantering.common.Role; +import org.example.projektarendehantering.infrastructure.persistence.EmployeeRepository; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.stereotype.Component; import java.nio.charset.StandardCharsets; @@ -16,6 +18,12 @@ @Component public class SecurityActorAdapter { + private final EmployeeRepository employeeRepository; + + public SecurityActorAdapter(EmployeeRepository employeeRepository) { + this.employeeRepository = employeeRepository; + } + public Actor currentUser() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); @@ -23,9 +31,26 @@ public Actor currentUser() { throw new NotAuthorizedException("User not authenticated"); } + // Try to find the username (GitHub 'login' attribute) or fallback to name + String name = authentication.getName(); + if (authentication instanceof OAuth2AuthenticationToken oauth2Token) { + String login = oauth2Token.getPrincipal().getAttribute("login"); + if (login != null) { + name = login; + } + } + // Create a deterministic UUID based on the username/name - UUID userId = UUID.nameUUIDFromBytes(authentication.getName().getBytes(StandardCharsets.UTF_8)); + UUID userId = UUID.nameUUIDFromBytes(name.getBytes(StandardCharsets.UTF_8)); + + // 1. Try finding an employee with this UUID + var employee = employeeRepository.findById(userId); + if (employee.isPresent()) { + var e = employee.get(); + return new Actor(userId, e.getRole(), e.getDisplayName(), e.getGithubUsername()); + } + // 2. Fallback to existing logic (checking Spring authorities) Role role = Role.PATIENT; if (authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_MANAGER"))) { role = Role.MANAGER; @@ -37,6 +62,6 @@ public Actor currentUser() { role = Role.PATIENT; } - return new Actor(userId, role); + return new Actor(userId, role, null, name); } } diff --git a/src/main/java/org/example/projektarendehantering/presentation/dto/CaseNoteDTO.java b/src/main/java/org/example/projektarendehantering/presentation/dto/CaseNoteDTO.java index 2f5660c..7619de3 100644 --- a/src/main/java/org/example/projektarendehantering/presentation/dto/CaseNoteDTO.java +++ b/src/main/java/org/example/projektarendehantering/presentation/dto/CaseNoteDTO.java @@ -7,15 +7,19 @@ public class CaseNoteDTO { private UUID id; private String content; - private String author; + private String authorDisplayName; + private String authorGithubUsername; + private String authorRole; private Instant createdAt; public CaseNoteDTO() {} - public CaseNoteDTO(UUID id, String content, String author, Instant createdAt) { + public CaseNoteDTO(UUID id, String content, String authorDisplayName, String authorGithubUsername, String authorRole, Instant createdAt) { this.id = id; this.content = content; - this.author = author; + this.authorDisplayName = authorDisplayName; + this.authorGithubUsername = authorGithubUsername; + this.authorRole = authorRole; this.createdAt = createdAt; } @@ -25,8 +29,14 @@ public CaseNoteDTO(UUID id, String content, String author, Instant createdAt) { public String getContent() { return content; } public void setContent(String content) { this.content = content; } - public String getAuthor() { return author; } - public void setAuthor(String author) { this.author = author; } + public String getAuthorDisplayName() { return authorDisplayName; } + public void setAuthorDisplayName(String authorDisplayName) { this.authorDisplayName = authorDisplayName; } + + public String getAuthorGithubUsername() { return authorGithubUsername; } + public void setAuthorGithubUsername(String authorGithubUsername) { this.authorGithubUsername = authorGithubUsername; } + + public String getAuthorRole() { return authorRole; } + public void setAuthorRole(String authorRole) { this.authorRole = authorRole; } public Instant getCreatedAt() { return createdAt; } public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } diff --git a/src/main/java/org/example/projektarendehantering/presentation/dto/EmployeeCreateDTO.java b/src/main/java/org/example/projektarendehantering/presentation/dto/EmployeeCreateDTO.java index 1b542f4..94fdecc 100644 --- a/src/main/java/org/example/projektarendehantering/presentation/dto/EmployeeCreateDTO.java +++ b/src/main/java/org/example/projektarendehantering/presentation/dto/EmployeeCreateDTO.java @@ -9,19 +9,26 @@ public class EmployeeCreateDTO { @NotBlank private String displayName; + @NotBlank + private String githubUsername; + @NotNull private Role role; public EmployeeCreateDTO() {} - public EmployeeCreateDTO(String displayName, Role role) { + public EmployeeCreateDTO(String displayName, String githubUsername, Role role) { this.displayName = displayName; + this.githubUsername = githubUsername; this.role = role; } public String getDisplayName() { return displayName; } public void setDisplayName(String displayName) { this.displayName = displayName; } + public String getGithubUsername() { return githubUsername; } + public void setGithubUsername(String githubUsername) { this.githubUsername = githubUsername; } + public Role getRole() { return role; } public void setRole(Role role) { this.role = role; } } diff --git a/src/main/java/org/example/projektarendehantering/presentation/dto/EmployeeDTO.java b/src/main/java/org/example/projektarendehantering/presentation/dto/EmployeeDTO.java index fcecf7f..b502395 100644 --- a/src/main/java/org/example/projektarendehantering/presentation/dto/EmployeeDTO.java +++ b/src/main/java/org/example/projektarendehantering/presentation/dto/EmployeeDTO.java @@ -9,14 +9,16 @@ public class EmployeeDTO { private UUID id; private String displayName; + private String githubUsername; private Role role; private Instant createdAt; public EmployeeDTO() {} - public EmployeeDTO(UUID id, String displayName, Role role, Instant createdAt) { + public EmployeeDTO(UUID id, String displayName, String githubUsername, Role role, Instant createdAt) { this.id = id; this.displayName = displayName; + this.githubUsername = githubUsername; this.role = role; this.createdAt = createdAt; } @@ -27,6 +29,9 @@ public EmployeeDTO(UUID id, String displayName, Role role, Instant createdAt) { public String getDisplayName() { return displayName; } public void setDisplayName(String displayName) { this.displayName = displayName; } + public String getGithubUsername() { return githubUsername; } + public void setGithubUsername(String githubUsername) { this.githubUsername = githubUsername; } + public Role getRole() { return role; } public void setRole(Role role) { this.role = role; } diff --git a/src/main/java/org/example/projektarendehantering/presentation/web/EmployeeUiController.java b/src/main/java/org/example/projektarendehantering/presentation/web/EmployeeUiController.java new file mode 100644 index 0000000..9b3c467 --- /dev/null +++ b/src/main/java/org/example/projektarendehantering/presentation/web/EmployeeUiController.java @@ -0,0 +1,54 @@ +package org.example.projektarendehantering.presentation.web; + +import jakarta.validation.Valid; +import org.example.projektarendehantering.application.service.EmployeeService; +import org.example.projektarendehantering.common.Actor; +import org.example.projektarendehantering.common.Role; +import org.example.projektarendehantering.infrastructure.security.SecurityActorAdapter; +import org.example.projektarendehantering.presentation.dto.EmployeeCreateDTO; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; + +@Controller +public class EmployeeUiController { + + private final EmployeeService employeeService; + private final SecurityActorAdapter securityActorAdapter; + + public EmployeeUiController(EmployeeService employeeService, SecurityActorAdapter securityActorAdapter) { + this.employeeService = employeeService; + this.securityActorAdapter = securityActorAdapter; + } + + @GetMapping("/ui/employees") + public String listEmployees(Model model) { + Actor actor = securityActorAdapter.currentUser(); + model.addAttribute("employees", employeeService.getAllEmployees(actor)); + return "employees/list"; + } + + @GetMapping("/ui/employees/new") + @PreAuthorize("hasRole('MANAGER')") + public String newEmployee(Model model) { + model.addAttribute("employeeCreateDTO", new EmployeeCreateDTO()); + model.addAttribute("roles", Role.values()); + return "employees/new"; + } + + @PostMapping("/ui/employees/new") + public String createEmployee(@Valid @ModelAttribute("employeeCreateDTO") EmployeeCreateDTO dto, BindingResult result, Model model) { + if (result.hasErrors()) { + model.addAttribute("roles", Role.values()); + return "employees/new"; + } + + Actor actor = securityActorAdapter.currentUser(); + employeeService.createEmployee(actor, dto); + return "redirect:/ui/employees"; + } +} diff --git a/src/main/java/org/example/projektarendehantering/presentation/web/GlobalControllerAdvice.java b/src/main/java/org/example/projektarendehantering/presentation/web/GlobalControllerAdvice.java new file mode 100644 index 0000000..d209135 --- /dev/null +++ b/src/main/java/org/example/projektarendehantering/presentation/web/GlobalControllerAdvice.java @@ -0,0 +1,26 @@ +package org.example.projektarendehantering.presentation.web; + +import org.example.projektarendehantering.common.Actor; +import org.example.projektarendehantering.common.NotAuthorizedException; +import org.example.projektarendehantering.infrastructure.security.SecurityActorAdapter; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ModelAttribute; + +@ControllerAdvice +public class GlobalControllerAdvice { + + private final SecurityActorAdapter securityActorAdapter; + + public GlobalControllerAdvice(SecurityActorAdapter securityActorAdapter) { + this.securityActorAdapter = securityActorAdapter; + } + + @ModelAttribute("currentActor") + public Actor currentActor() { + try { + return securityActorAdapter.currentUser(); + } catch (NotAuthorizedException e) { + return null; + } + } +} diff --git a/src/main/java/org/example/projektarendehantering/presentation/web/UiController.java b/src/main/java/org/example/projektarendehantering/presentation/web/UiController.java index 6e91bed..3d49529 100644 --- a/src/main/java/org/example/projektarendehantering/presentation/web/UiController.java +++ b/src/main/java/org/example/projektarendehantering/presentation/web/UiController.java @@ -32,9 +32,8 @@ public UiController(CaseService caseService, PatientService patientService, Secu } @PostMapping("/ui/cases/{caseId}/notes") - public String addNote(@PathVariable UUID caseId, @RequestParam("content") String content, Principal principal) { - String author = principal != null ? principal.getName() : "Anonymous"; - caseService.addNote(caseId, content, author); + public String addNote(@PathVariable UUID caseId, @RequestParam("content") String content) { + caseService.addNote(caseId, content, securityActorAdapter.currentUser()); return "redirect:/ui/cases/" + caseId; } diff --git a/src/main/resources/templates/cases/detail.html b/src/main/resources/templates/cases/detail.html index dd18676..880a592 100644 --- a/src/main/resources/templates/cases/detail.html +++ b/src/main/resources/templates/cases/detail.html @@ -31,7 +31,7 @@

Notes

Note content

- Author - + Author - Date
diff --git a/src/main/resources/templates/employees/list.html b/src/main/resources/templates/employees/list.html new file mode 100644 index 0000000..d9f35a8 --- /dev/null +++ b/src/main/resources/templates/employees/list.html @@ -0,0 +1,42 @@ + + + + + +
+ +
+
+

User Mappings (Employees)

+ New Mapping +
+ +
+

List of GitHub users mapped to application roles.

+ + + + + + + + + + + + + + + + + + + +
Display NameGitHub UsernameRoleID (UUID)Created At
NameUsernameRoleUUIDDate
+

No user mappings found.

+
+
+ +
+ + diff --git a/src/main/resources/templates/employees/new.html b/src/main/resources/templates/employees/new.html new file mode 100644 index 0000000..fa717c4 --- /dev/null +++ b/src/main/resources/templates/employees/new.html @@ -0,0 +1,43 @@ + + + + + +
+ +
+
+

New User Mapping

+ Back to list +
+ +
+ + + +
+ +
+
+
+ +
+ + diff --git a/src/main/resources/templates/fragments/header.html b/src/main/resources/templates/fragments/header.html index 4a4756b..0b93cd2 100644 --- a/src/main/resources/templates/fragments/header.html +++ b/src/main/resources/templates/fragments/header.html @@ -11,11 +11,16 @@ Cases Create Audit - + Manage Users
- User + User
diff --git a/src/test/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapterTest.java b/src/test/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapterTest.java new file mode 100644 index 0000000..be29a13 --- /dev/null +++ b/src/test/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapterTest.java @@ -0,0 +1,188 @@ +package org.example.projektarendehantering.infrastructure.security; + +import org.example.projektarendehantering.common.Actor; +import org.example.projektarendehantering.common.NotAuthorizedException; +import org.example.projektarendehantering.common.Role; +import org.example.projektarendehantering.infrastructure.persistence.EmployeeEntity; +import org.example.projektarendehantering.infrastructure.persistence.EmployeeRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class SecurityActorAdapterTest { + + @Mock + private EmployeeRepository employeeRepository; + + @Mock + private Authentication authentication; + + @Mock + private SecurityContext securityContext; + + @InjectMocks + private SecurityActorAdapter securityActorAdapter; + + @BeforeEach + void setUp() { + SecurityContextHolder.setContext(securityContext); + } + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + void currentUser_whenNotAuthenticated_shouldThrowException() { + when(securityContext.getAuthentication()).thenReturn(null); + + assertThrows(NotAuthorizedException.class, () -> securityActorAdapter.currentUser()); + } + + @Test + void currentUser_whenAnonymousUser_shouldThrowException() { + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn("anonymousUser"); + + assertThrows(NotAuthorizedException.class, () -> securityActorAdapter.currentUser()); + } + + @Test + void currentUser_whenEmployeeFoundInRepository_shouldReturnActorFromEmployee() { + String username = "testuser"; + UUID userId = UUID.nameUUIDFromBytes(username.getBytes(StandardCharsets.UTF_8)); + EmployeeEntity employee = new EmployeeEntity(userId, "Test User", username, Role.DOCTOR, Instant.now()); + + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(username); + when(employeeRepository.findById(userId)).thenReturn(Optional.of(employee)); + + Actor actor = securityActorAdapter.currentUser(); + + assertEquals(userId, actor.userId()); + assertEquals(Role.DOCTOR, actor.role()); + assertEquals("Test User", actor.displayName()); + assertEquals(username, actor.githubUsername()); + } + + @Test + void currentUser_whenOAuth2Authentication_shouldUseLoginAttribute() { + String login = "oauth-user"; + UUID userId = UUID.nameUUIDFromBytes(login.getBytes(StandardCharsets.UTF_8)); + + OAuth2AuthenticationToken oauth2Token = mock(OAuth2AuthenticationToken.class); + OAuth2User oauth2User = mock(OAuth2User.class); + + when(securityContext.getAuthentication()).thenReturn(oauth2Token); + when(oauth2Token.isAuthenticated()).thenReturn(true); + when(oauth2Token.getName()).thenReturn("some-other-name"); + when(oauth2Token.getPrincipal()).thenReturn(oauth2User); + when(oauth2User.getAttribute("login")).thenReturn(login); + + when(employeeRepository.findById(userId)).thenReturn(Optional.empty()); + doReturn(Collections.emptyList()).when(oauth2Token).getAuthorities(); + + Actor actor = securityActorAdapter.currentUser(); + + assertEquals(userId, actor.userId()); + assertEquals(login, actor.githubUsername()); + assertEquals(Role.PATIENT, actor.role()); + } + + @Test + void currentUser_whenEmployeeNotFound_shouldFallbackToAuthorities_Manager() { + String username = "manager-user"; + UUID userId = UUID.nameUUIDFromBytes(username.getBytes(StandardCharsets.UTF_8)); + + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(username); + when(employeeRepository.findById(userId)).thenReturn(Optional.empty()); + + doReturn(List.of(new SimpleGrantedAuthority("ROLE_MANAGER"))) + .when(authentication).getAuthorities(); + + Actor actor = securityActorAdapter.currentUser(); + + assertEquals(Role.MANAGER, actor.role()); + assertEquals(username, actor.githubUsername()); + assertNull(actor.displayName()); + } + + @Test + void currentUser_whenEmployeeNotFound_shouldFallbackToAuthorities_Doctor() { + String username = "doctor-user"; + UUID userId = UUID.nameUUIDFromBytes(username.getBytes(StandardCharsets.UTF_8)); + + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(username); + when(employeeRepository.findById(userId)).thenReturn(Optional.empty()); + + doReturn(List.of(new SimpleGrantedAuthority("ROLE_DOCTOR"))) + .when(authentication).getAuthorities(); + + Actor actor = securityActorAdapter.currentUser(); + + assertEquals(Role.DOCTOR, actor.role()); + } + + @Test + void currentUser_whenEmployeeNotFound_shouldFallbackToAuthorities_Nurse() { + String username = "nurse-user"; + UUID userId = UUID.nameUUIDFromBytes(username.getBytes(StandardCharsets.UTF_8)); + + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(username); + when(employeeRepository.findById(userId)).thenReturn(Optional.empty()); + + doReturn(List.of(new SimpleGrantedAuthority("ROLE_NURSE"))) + .when(authentication).getAuthorities(); + + Actor actor = securityActorAdapter.currentUser(); + + assertEquals(Role.NURSE, actor.role()); + } + + @Test + void currentUser_whenEmployeeNotFoundAndNoRoles_shouldDefaultToPatient() { + String username = "patient-user"; + UUID userId = UUID.nameUUIDFromBytes(username.getBytes(StandardCharsets.UTF_8)); + + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(username); + when(employeeRepository.findById(userId)).thenReturn(Optional.empty()); + + doReturn(Collections.emptyList()).when(authentication).getAuthorities(); + + Actor actor = securityActorAdapter.currentUser(); + + assertEquals(Role.PATIENT, actor.role()); + } +}