diff --git a/src/main/java/org/example/alfs/config/GlobalModelAttributes.java b/src/main/java/org/example/alfs/config/GlobalModelAttributes.java new file mode 100644 index 0000000..cf9cf9d --- /dev/null +++ b/src/main/java/org/example/alfs/config/GlobalModelAttributes.java @@ -0,0 +1,35 @@ +package org.example.alfs.config; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.example.alfs.security.SecurityUtils; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.ModelAttribute; + +@ControllerAdvice +public class GlobalModelAttributes { + private static final Logger log = LoggerFactory.getLogger(GlobalModelAttributes.class); + private final SecurityUtils securityUtils; + + public GlobalModelAttributes(SecurityUtils securityUtils) { + this.securityUtils = securityUtils; + } + + @ModelAttribute + public void addGlobalAttributes(Model model) { + + boolean isLoggedIn = false; + String username = null; + + try { + var user = securityUtils.getCurrentUser(); + isLoggedIn = true; + username = user.getUsername(); + } catch (RuntimeException ex) { + log.debug("Could not resolve current user for global model attributes", ex); + } + + model.addAttribute("isLoggedIn", isLoggedIn); + model.addAttribute("username", username); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/alfs/config/SecurityConfig.java b/src/main/java/org/example/alfs/config/SecurityConfig.java index 0b61490..e2565e7 100644 --- a/src/main/java/org/example/alfs/config/SecurityConfig.java +++ b/src/main/java/org/example/alfs/config/SecurityConfig.java @@ -41,11 +41,15 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .authorizeHttpRequests(auth -> auth .requestMatchers("/auth/login").permitAll() .requestMatchers("/auth/signup").permitAll() + .requestMatchers("/auth/logout").permitAll() .requestMatchers("/auth/hash").permitAll() .requestMatchers("/h2-console/**").permitAll() + .requestMatchers("/startPage", "/").permitAll() + .requestMatchers("/tickets/create").permitAll() + .requestMatchers("/tickets/previewTicket").permitAll() //allow access to endpoints during development - .requestMatchers("/create", "/tickets/**", "/view/**").permitAll() + .requestMatchers("/tickets/**").permitAll() .requestMatchers("/css/**", "/js/**", "/images/**", "/static/**").permitAll() .requestMatchers("/login", "/login-form").permitAll() .requestMatchers("/signup", "/signup-form").permitAll() diff --git a/src/main/java/org/example/alfs/controllers/AuthViewController.java b/src/main/java/org/example/alfs/controllers/AuthViewController.java index 990cea0..5713b82 100644 --- a/src/main/java/org/example/alfs/controllers/AuthViewController.java +++ b/src/main/java/org/example/alfs/controllers/AuthViewController.java @@ -1,6 +1,6 @@ package org.example.alfs.controllers; -import jakarta.servlet.http.Cookie; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.example.alfs.dto.auth.SignupRequestDTO; @@ -8,11 +8,15 @@ import org.example.alfs.security.JwtService; import org.example.alfs.services.AuthService; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; +import java.time.Duration; + /** * Handles login for the browser (UI). * @@ -32,8 +36,10 @@ public AuthViewController(AuthService authService, JwtService jwtService) { } @GetMapping("/login") - public String loginPage() { - return "login"; // login.jte + public String loginPage(@RequestParam(required = false) String error, @RequestParam(required = false) String tokenError, Model model) { + model.addAttribute("error", error); + model.addAttribute("tokenError", tokenError); + return "login"; } @GetMapping("/signup") @@ -61,21 +67,25 @@ public String signupForm( public String loginForm( @RequestParam String username, @RequestParam String password, - HttpServletResponse response + HttpServletResponse response, + RedirectAttributes redirectAttributes ) { try { User user = authService.login(username, password); String token = jwtService.generateToken(user); - Cookie cookie = new Cookie("JWT", token); - cookie.setHttpOnly(true); - cookie.setPath("/"); - cookie.setMaxAge(60 * 60 * 24); + ResponseCookie cookie = ResponseCookie.from("JWT", token) + .httpOnly(true) + .path("/") + .maxAge(Duration.ofDays(1)) + .sameSite("Lax") + .build(); - response.addCookie(cookie); + response.addHeader("Set-Cookie", cookie.toString()); - return "redirect:/api/hello"; // should change later + redirectAttributes.addFlashAttribute("success", "You are signed in!"); + return "redirect:/"; } catch (ResponseStatusException ex) { @@ -89,16 +99,18 @@ public String loginForm( } } - @PostMapping("/logout") - public String logout(HttpServletResponse response) { - - Cookie cookie = new Cookie("JWT", null); - cookie.setHttpOnly(true); - cookie.setPath("/"); - cookie.setMaxAge(0); + @PostMapping("/auth/logout") + public String logout(HttpServletResponse response, RedirectAttributes redirectAttributes) { - response.addCookie(cookie); + ResponseCookie cookie = ResponseCookie.from("JWT", "") + .httpOnly(true) + .path("/") + .maxAge(Duration.ZERO) + .sameSite("Lax") + .build(); - return "redirect:/login"; + response.addHeader("Set-Cookie", cookie.toString()); + redirectAttributes.addFlashAttribute("success", "Successfully signed out"); + return "redirect:/"; } } \ No newline at end of file diff --git a/src/main/java/org/example/alfs/controllers/StartPageController.java b/src/main/java/org/example/alfs/controllers/StartPageController.java new file mode 100644 index 0000000..a41a840 --- /dev/null +++ b/src/main/java/org/example/alfs/controllers/StartPageController.java @@ -0,0 +1,14 @@ +package org.example.alfs.controllers; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class StartPageController { + + @GetMapping("/") + public String startPage(Model model){ + return "startPage"; + } +} diff --git a/src/main/java/org/example/alfs/controllers/TicketController.java b/src/main/java/org/example/alfs/controllers/TicketController.java index 7ff0a32..0701b52 100644 --- a/src/main/java/org/example/alfs/controllers/TicketController.java +++ b/src/main/java/org/example/alfs/controllers/TicketController.java @@ -1,5 +1,7 @@ package org.example.alfs.controllers; - +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; import jakarta.validation.Valid; import org.example.alfs.dto.ticket.TicketAssignDTO; import org.example.alfs.dto.ticket.TicketCreateDTO; @@ -27,36 +29,56 @@ public TicketController(TicketService ticketService) { } //create ticket - @PreAuthorize("hasRole('REPORTER')") // should change later for anonymous access + //@PreAuthorize("hasRole('REPORTER')") // should change later for anonymous access @GetMapping("/create") public String createNewTicketForm(Model model) { model.addAttribute("ticket", new TicketCreateDTO()); return "create"; } - @PreAuthorize("hasRole('REPORTER')") // should change later for anonymous access + //@PreAuthorize("hasRole('REPORTER')") // should change later for anonymous access @PostMapping("/create") - public String createNewTicket(@ModelAttribute("ticket") @Valid TicketCreateDTO ticketCreateDTO, BindingResult bindingResult, Model model) { + public String createNewTicket( + @ModelAttribute("ticket") @Valid TicketCreateDTO dto, + BindingResult bindingResult, + Model model, + RedirectAttributes redirectAttributes + ) { if (bindingResult.hasErrors()) { - model.addAttribute("ticket", ticketCreateDTO); + model.addAttribute("ticket", dto); return "create"; } - TicketViewDTO ticket = ticketService.createNewTicket(ticketCreateDTO); + TicketViewDTO ticket = ticketService.createNewTicket(dto); + redirectAttributes.addFlashAttribute("success", "Ticket created successfully"); - return "redirect:/tickets/" + ticket.getId(); + if (ticket.getToken() != null) { + return "redirect:/tickets/ticket-created?token=" + ticket.getToken(); + } + return "redirect:/tickets/" + ticket.getId(); } - //view ticket by token + + + //view ticket by token @GetMapping("/token/{token}") public String viewTicketByToken(@PathVariable String token, Model model) { - TicketViewDTO ticket = ticketService.getTicketByToken(token); - model.addAttribute("ticket", ticket); + try { + TicketViewDTO ticket = ticketService.getTicketByToken(token); + model.addAttribute("ticket", ticket); + return "view"; - return "view"; + } catch (ResponseStatusException ex) { + + if (ex.getStatusCode() == HttpStatus.NOT_FOUND) { + return "redirect:/login?tokenError=true"; + } + + throw ex; + } } //view ticket by id @@ -108,6 +130,13 @@ public String myAssignedTickets(Model model) { return "assigned-tickets"; } + + @GetMapping("/ticket-created") + public String ticketCreated(@RequestParam String token, Model model) { + model.addAttribute("token", token); + return "ticket-created"; + } + //create comment //View comment //upload attachment diff --git a/src/main/java/org/example/alfs/dto/ticket/TicketViewDTO.java b/src/main/java/org/example/alfs/dto/ticket/TicketViewDTO.java index 2b1108e..d84539f 100644 --- a/src/main/java/org/example/alfs/dto/ticket/TicketViewDTO.java +++ b/src/main/java/org/example/alfs/dto/ticket/TicketViewDTO.java @@ -2,7 +2,7 @@ import lombok.Data; import org.example.alfs.enums.TicketStatus; - +import java.time.format.DateTimeFormatter; import java.time.LocalDateTime; /* @@ -17,10 +17,17 @@ public class TicketViewDTO { private Long id; + private String token; private String title; private String description; private TicketStatus status; private LocalDateTime createdAt; private Long assignedInvestigatorId; + + public String getFormattedCreatedAt() { + if (createdAt == null) return ""; + + return createdAt.format(DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm")); + } } diff --git a/src/main/java/org/example/alfs/entities/Ticket.java b/src/main/java/org/example/alfs/entities/Ticket.java index b159aaa..6db2207 100644 --- a/src/main/java/org/example/alfs/entities/Ticket.java +++ b/src/main/java/org/example/alfs/entities/Ticket.java @@ -36,7 +36,7 @@ public class Ticket { @Column(nullable = false, length = 32) private TicketStatus status; - @Column(nullable = false, unique = true, length = 128, updatable = false) + @Column(nullable = true, unique = true, length = 128, updatable = false) private String reporterToken; private LocalDateTime createdAt; @@ -47,8 +47,6 @@ public class Ticket { public void prePersist() { createdAt = LocalDateTime.now(); if (status == null) status = TicketStatus.OPEN; - if (reporterToken == null || reporterToken.isBlank()) - reporterToken = UUID.randomUUID().toString(); // Skapa token för anonyma anmälare } @PreUpdate diff --git a/src/main/java/org/example/alfs/security/JwtAuthenticationFilter.java b/src/main/java/org/example/alfs/security/JwtAuthenticationFilter.java index 274d89f..a1d8e44 100644 --- a/src/main/java/org/example/alfs/security/JwtAuthenticationFilter.java +++ b/src/main/java/org/example/alfs/security/JwtAuthenticationFilter.java @@ -26,11 +26,11 @@ public JwtAuthenticationFilter(JwtService jwtService, UserRepository userReposit this.userRepository = userRepository; } - // Skips filter for LOGIN & H2 + // Skips filter for LOGIN & H2 & START PAGE @Override protected boolean shouldNotFilter(HttpServletRequest request) { String path = request.getRequestURI(); - return path.startsWith("/auth") || path.startsWith("/h2-console"); + return path.startsWith("/auth") || path.startsWith("/h2-console") || path.startsWith("/startPage"); } diff --git a/src/main/java/org/example/alfs/security/SecurityUtils.java b/src/main/java/org/example/alfs/security/SecurityUtils.java index b2ed239..9d76be1 100644 --- a/src/main/java/org/example/alfs/security/SecurityUtils.java +++ b/src/main/java/org/example/alfs/security/SecurityUtils.java @@ -27,4 +27,23 @@ public User getCurrentUser() { return userRepository.findByUsername(username) .orElseThrow(() -> new RuntimeException("Authenticated user not found in database")); } + + public User getCurrentUserOrNull() { + try { + return getCurrentUser(); + } catch (RuntimeException ex) { + + String message = ex.getMessage(); + + boolean authFailure = + "No authenticated user in security context".equals(message) || + "Authenticated user not found in database".equals(message); + + if (authFailure) { + return null; + } + + throw ex; + } + } } \ No newline at end of file diff --git a/src/main/java/org/example/alfs/services/TicketService.java b/src/main/java/org/example/alfs/services/TicketService.java index 27ee9bb..6114dfe 100644 --- a/src/main/java/org/example/alfs/services/TicketService.java +++ b/src/main/java/org/example/alfs/services/TicketService.java @@ -38,27 +38,46 @@ public TicketService(TicketRepository ticketRepository, } //createNewTicket - public TicketViewDTO createNewTicket(TicketCreateDTO ticketCreateDTO) { + public TicketViewDTO createNewTicket(TicketCreateDTO dto) { Ticket ticket = new Ticket(); - ticket.setTitle(ticketCreateDTO.getTitle()); - ticket.setDescription(ticketCreateDTO.getDescription()); + ticket.setTitle(dto.getTitle()); + ticket.setDescription(dto.getDescription()); - User user = requireCurrentUser(); - ticket.setReporter(user); + User user = securityUtils.getCurrentUserOrNull(); - Ticket savedTicket = ticketRepository.save(ticket); + String token = null; - return ticketMapper.entityToViewDTO(savedTicket); + if (user != null) { + ticket.setReporter(user); + } else { + token = java.util.UUID.randomUUID().toString(); + ticket.setReporterToken(token); + } + + Ticket saved = ticketRepository.save(ticket); + + TicketViewDTO view = ticketMapper.entityToViewDTO(saved); + + if (token != null) { + view.setToken(token); + } + + return view; } + // View by token public TicketViewDTO getTicketByToken(String token) { Ticket ticket = ticketRepository.findByReporterToken(token) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found")); - return ticketMapper.entityToViewDTO(ticket); + TicketViewDTO view = ticketMapper.entityToViewDTO(ticket); + + view.setToken(ticket.getReporterToken()); + + return view; } //findById diff --git a/src/main/jte/create.jte b/src/main/jte/create.jte index 02fc472..0441912 100644 --- a/src/main/jte/create.jte +++ b/src/main/jte/create.jte @@ -1,8 +1,20 @@ @param org.example.alfs.dto.ticket.TicketCreateDTO ticket - +@param String success +@param Boolean isLoggedIn +@param String username @template.layout(title = "Create Ticket", content = @`

Create Ticket

- + @if(!isLoggedIn) +
+ This ticket will be created anonymously. + Sign in to attach it to your account. +
+ @else +
+ This ticket will be created as ${username}. + To create anonymously, please sign out first. +
+ @endif
-`) \ No newline at end of file +`, success = success) \ No newline at end of file diff --git a/src/main/jte/layout.jte b/src/main/jte/layout.jte index 1202e22..78407ed 100644 --- a/src/main/jte/layout.jte +++ b/src/main/jte/layout.jte @@ -1,5 +1,7 @@ +@import org.springframework.security.core.context.SecurityContextHolder @param String title @param gg.jte.Content content +@param String success @@ -21,15 +23,39 @@
-

- ALFS - whistleblower system -

+ +

+ ALFS + whistleblower system +

+
+ +
@@ -48,5 +74,48 @@ + + @if(success != null) +
+ +
+ + +
+ + +
+ + +
+ ${success} +
+ + + +
+ + +
+
+
+@endif + + + + \ No newline at end of file diff --git a/src/main/jte/login.jte b/src/main/jte/login.jte index 08682ae..ba43e02 100644 --- a/src/main/jte/login.jte +++ b/src/main/jte/login.jte @@ -1,15 +1,68 @@ -

Login

+@param String error +@param String tokenError +@param String success +@template.layout(title = "Login", content = @` +
+

Login

+ +
+ @if(error != null) +
+ Invalid username or password +
+ @endif +
+ + + + +
+ + + +
+ + + +

+ Don’t have an account? + + Create account here + +

+
+ -
-
- -
+

Got a token?

+
+ @if(tokenError != null) +
+ Invalid token +
+ @endif + + +

View report with token

+ + + + -
- - +
+
- - \ No newline at end of file +`, success = success) \ No newline at end of file diff --git a/src/main/jte/my-tickets.jte b/src/main/jte/my-tickets.jte new file mode 100644 index 0000000..6717366 --- /dev/null +++ b/src/main/jte/my-tickets.jte @@ -0,0 +1,38 @@ +@param java.util.List tickets +@param String success + +@template.layout(title = "My tickets", content = @` + +

My tickets

+ +@if(tickets.isEmpty()) +

No tickets yet.

+@else +
+ @for (var ticket : tickets) +
+ +

+ ${ticket.getTitle()} +

+ +

+ ${ticket.getDescription()} +

+ +

+ Created: ${ticket.getFormattedCreatedAt()} +

+

Status: ${ticket.getStatus()}

+ + + View ticket → + + +
+ @endfor +
+@endif + +`, success = success) \ No newline at end of file diff --git a/src/main/jte/previewTicket.jte b/src/main/jte/previewTicket.jte new file mode 100644 index 0000000..ba03a11 --- /dev/null +++ b/src/main/jte/previewTicket.jte @@ -0,0 +1,30 @@ +@param org.example.alfs.dto.ticket.TicketCreateDTO ticket +@param String success + +@template.layout(title = "Preview", content = @` + +

Preview

+ +
+

${ticket.getTitle()}

+

${ticket.getDescription()}

+
+ + +
+ + +
+ + + + +
+ +
+ + + +`, success = success) \ No newline at end of file diff --git a/src/main/jte/signup.jte b/src/main/jte/signup.jte index 8e441f9..cf43aa4 100644 --- a/src/main/jte/signup.jte +++ b/src/main/jte/signup.jte @@ -1,17 +1,37 @@ -

Sign up

+@param org.example.alfs.dto.auth.SignupRequestDTO request +@param String success + +@template.layout(title = "Sign up", content = @` + +

Sign up

+ +
-
- - + +
- - + +
- + + +

+ Already have an account? + + Login + +

+
-Already have an account? Login \ No newline at end of file +`, success = success) \ No newline at end of file diff --git a/src/main/jte/startPage.jte b/src/main/jte/startPage.jte new file mode 100644 index 0000000..25a4c8c --- /dev/null +++ b/src/main/jte/startPage.jte @@ -0,0 +1,54 @@ +@param String success +@template.layout(title = "Start Page", content = @` + + +
+ +
+

+ Welcome +

+ +

+ Here you can anonymously and securely report misconduct that concerns the public interest. + We guarantee that your identity is protected in accordance with applicable whistleblowing legislation. +

+
+ +
+ +
+

High security

+

This system is designed to protect your confidentiality and report integrity.

+
+ +
+

Anonymity

+

You do not need to provide personal information to submit a report.

+
+ +
+

Statutory protection

+

Protection against retaliation under national directives and regulations.

+
+ +
+ + +
+ +`, success = success) \ No newline at end of file diff --git a/src/main/jte/ticket-created.jte b/src/main/jte/ticket-created.jte new file mode 100644 index 0000000..9b5a453 --- /dev/null +++ b/src/main/jte/ticket-created.jte @@ -0,0 +1,24 @@ +@param String token +@param String success + +@template.layout(title = "Ticket Created", content = @` + +

Ditt ärende är skapat!

+ +
+ +

+ Spara denna kod: + ${token} +

+ +

+ + Gå till ditt ärende + +

+ +
+ +`, success = success) \ No newline at end of file diff --git a/src/main/jte/view.jte b/src/main/jte/view.jte index 60b2417..06964a5 100644 --- a/src/main/jte/view.jte +++ b/src/main/jte/view.jte @@ -1,4 +1,5 @@ @param org.example.alfs.dto.ticket.TicketViewDTO ticket +@param String success @template.layout(title = "View Ticket", content = @`

Ticket

@@ -20,4 +21,4 @@ Status: ${ticket.getStatus()}
-`) \ No newline at end of file +`, success = success) \ No newline at end of file diff --git a/src/main/resources/static/css/style.css b/src/main/resources/static/css/style.css index 93091c2..7a99866 100644 --- a/src/main/resources/static/css/style.css +++ b/src/main/resources/static/css/style.css @@ -1,27 +1,42 @@ +@import "https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap"; :root { --state-blue: #1a2b49; --state-gold: #b39359; --state-gray: #f4f4f4; } + body { - font-family: Georgia, serif; + font-family: Roboto, sans-serif; background-color: var(--state-gray); color: #333; } + .header-border { border-bottom: 4px solid var(--state-gold); } + .btn-primary { background-color: var(--state-blue); color: white; transition: all 0.3s ease; } + .btn-primary:hover { background-color: #2c4471; } + .card { background: white; border: 1px solid #ddd; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); } + +@keyframes toast-progress { + from { width: 100%; } + to { width: 0%; } +} + +.animate-toast { + animation: toast-progress 3s linear forwards; +} \ No newline at end of file diff --git a/src/test/java/org/example/alfs/services/TicketServiceTest.java b/src/test/java/org/example/alfs/services/TicketServiceTest.java index a85eae1..2a48ef1 100644 --- a/src/test/java/org/example/alfs/services/TicketServiceTest.java +++ b/src/test/java/org/example/alfs/services/TicketServiceTest.java @@ -14,6 +14,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -71,28 +72,63 @@ private User reporterUser() { return u; } - @Test - @DisplayName("createNewTicket should set reporter as user") - void createNewTicket_shouldSetReporterAsUser() { - // Arrange - TicketCreateDTO dto = new TicketCreateDTO(); - dto.setTitle("Test Ticket"); - dto.setDescription("This is a test ticket"); - User reporter = reporterUser(); - - when(securityUtils.getCurrentUser()).thenReturn(reporter); - when(ticketRepository.save(any())).thenAnswer(i -> i.getArgument(0)); - when(ticketMapper.entityToViewDTO(any())).thenReturn(new TicketViewDTO()); - - // Act - ticketService.createNewTicket(dto); - - // Assert - verify(ticketRepository).save(argThat(ticket -> - ticket.getReporter().equals(reporter) && - ticket.getTitle().equals("Test Ticket") && - ticket.getDescription().equals("This is a test ticket") - )); + @Nested + @DisplayName("createNewTicket tests") + class CreateNewTicketTests { + + @Test + @DisplayName("createNewTicket should set reporter as user") + void createNewTicket_shouldSetReporterAsUser() { + // Arrange + TicketCreateDTO dto = new TicketCreateDTO(); + dto.setTitle("Test Ticket"); + dto.setDescription("This is a test ticket"); + User reporter = reporterUser(); + + TicketViewDTO viewDTO = new TicketViewDTO(); + + when(securityUtils.getCurrentUserOrNull()).thenReturn(reporter); + when(ticketRepository.save(any())).thenAnswer(i -> i.getArgument(0)); + when(ticketMapper.entityToViewDTO(any())).thenReturn(viewDTO); + + // Act + TicketViewDTO result = ticketService.createNewTicket(dto); + + // Assert + verify(ticketRepository).save(argThat(ticket -> + reporter.equals(ticket.getReporter()) && + ticket.getReporterToken() == null && + "Test Ticket".equals(ticket.getTitle()) && + "This is a test ticket".equals(ticket.getDescription()) + )); + assertNull(result.getToken()); + } + + @Test + @DisplayName("createNewTicket should set reporter token when user is not authenticated") + void createNewTicket_shouldSetReporterToken_whenAnonymous() { + // Arrange + TicketCreateDTO dto = new TicketCreateDTO(); + dto.setTitle("Anonymous Ticket"); + dto.setDescription("Filed anonymously"); + + TicketViewDTO viewDTO = new TicketViewDTO(); + + when(securityUtils.getCurrentUserOrNull()).thenReturn(null); + when(ticketRepository.save(any())).thenAnswer(i -> i.getArgument(0)); + when(ticketMapper.entityToViewDTO(any())).thenReturn(viewDTO); + + // Act + ArgumentCaptor ticketCaptor = ArgumentCaptor.forClass(Ticket.class); + TicketViewDTO result = ticketService.createNewTicket(dto); + + // Assert + verify(ticketRepository).save(ticketCaptor.capture()); + Ticket saved = ticketCaptor.getValue(); + assertNull(saved.getReporter()); + assertNotNull(saved.getReporterToken()); + assertEquals(saved.getReporterToken(), result.getToken()); + } } @Nested @@ -474,10 +510,11 @@ void noAuthenticatedUser_shouldThrowUnauthorized() { // Act ResponseStatusException ex = assertThrows(ResponseStatusException.class, () -> - ticketService.createNewTicket(new TicketCreateDTO())); + ticketService.getMyTickets()); // Assert assertEquals(HttpStatus.UNAUTHORIZED, ex.getStatusCode()); + verify(ticketRepository, never()).findByReporterId(any()); } @Test @@ -489,10 +526,11 @@ void userNotFoundInDatabase_shouldThrowUnauthorized() { // Act ResponseStatusException ex = assertThrows(ResponseStatusException.class, () -> - ticketService.createNewTicket(new TicketCreateDTO())); + ticketService.getMyTickets()); // Assert assertEquals(HttpStatus.UNAUTHORIZED, ex.getStatusCode()); + verify(ticketRepository, never()).findByReporterId(any()); } }