download(
+ @Parameter(description = "ID of the attachment", example = "1") @PathVariable Long id) {
- Attachment att = attachmentService.getAttachmentById(id);
-
- final GetObjectResponse object;
- try {
- object = storageService.download(att.getS3Key());
- } catch (Exception ex) {
- throw new ResponseStatusException(
- HttpStatus.BAD_GATEWAY,
- "Failed to download attachment content",
- ex
- );
- }
+ Attachment att = attachmentService.getAttachmentById(id);
- String fileName = att.getFileName() != null ? att.getFileName() : "file";
+ final GetObjectResponse object;
+ try {
+ object = storageService.download(att.getS3Key());
+ } catch (Exception ex) {
+ throw new ResponseStatusException(
+ HttpStatus.BAD_GATEWAY, "Failed to download attachment content", ex);
+ }
- String contentDisposition = ContentDisposition.builder("attachment")
- .filename(fileName, StandardCharsets.UTF_8)
- .build()
- .toString();
+ String fileName = att.getFileName() != null ? att.getFileName() : "file";
- InputStreamResource resource = new InputStreamResource(object);
+ String contentDisposition =
+ ContentDisposition.builder("attachment")
+ .filename(fileName, StandardCharsets.UTF_8)
+ .build()
+ .toString();
- long contentLength = object.headers().get("Content-Length") != null
- ? Long.parseLong(object.headers().get("Content-Length"))
- : -1;
+ InputStreamResource resource = new InputStreamResource(object);
- ResponseEntity.BodyBuilder builder = ResponseEntity.ok()
- .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
- .contentType(MediaType.APPLICATION_OCTET_STREAM);
+ long contentLength =
+ object.headers().get("Content-Length") != null
+ ? Long.parseLong(object.headers().get("Content-Length"))
+ : -1;
- if (contentLength > 0) {
- builder.contentLength(contentLength);
- }
+ ResponseEntity.BodyBuilder builder =
+ ResponseEntity.ok()
+ .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
+ .contentType(MediaType.APPLICATION_OCTET_STREAM);
- return builder.body(resource);
+ if (contentLength > 0) {
+ builder.contentLength(contentLength);
}
-}
\ No newline at end of file
+
+ return builder.body(resource);
+ }
+}
diff --git a/src/main/java/org/example/alfs/controllers/AuthController.java b/src/main/java/org/example/alfs/controllers/AuthController.java
index 934e383..585e554 100644
--- a/src/main/java/org/example/alfs/controllers/AuthController.java
+++ b/src/main/java/org/example/alfs/controllers/AuthController.java
@@ -5,8 +5,7 @@
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
-import jakarta.servlet.http.Cookie;
-import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
import org.example.alfs.dto.auth.LoginRequestDTO;
import org.example.alfs.dto.auth.LoginResponseDTO;
import org.example.alfs.dto.auth.SignupRequestDTO;
@@ -14,89 +13,78 @@
import org.example.alfs.security.JwtService;
import org.example.alfs.services.AuthService;
import org.springframework.web.bind.annotation.*;
-import jakarta.validation.Valid;
-
/**
* REST-based authentication controller used for API clients such as Postman.
*
- * This controller handles JSON-based authentication requests and returns JWT tokens.
+ * This controller handles JSON-based authentication requests and returns JWT tokens.
*
- * NOTE:
- * The application also contains a separate AuthViewController which handles
- * browser-based login using HTML forms and cookies.
+ *
NOTE: The application also contains a separate AuthViewController which handles browser-based
+ * login using HTML forms and cookies.
*
- * We intentionally separate these concerns:
+ *
We intentionally separate these concerns:
*
- * - AuthController → API (JSON, used for testing and potential future clients)
- * - AuthViewController → UI (HTML forms, browser login flow)
+ *
- AuthController → API (JSON, used for testing and potential future clients) -
+ * AuthViewController → UI (HTML forms, browser login flow)
*
- * This separation keeps the API clean and allows independent development
- * of backend logic and user interface.
+ *
This separation keeps the API clean and allows independent development of backend logic and
+ * user interface.
*/
@RestController
@RequestMapping("/auth")
public class AuthController {
- private final AuthService authService;
- private final JwtService jwtService;
-
- public AuthController(AuthService authService, JwtService jwtService) {
- this.authService = authService;
- this.jwtService = jwtService;
- }
-
- /**
- * Authenticates a user using JSON input and returns a JWT.
- *
- * This endpoint is mainly used for API testing (e.g. Postman).
- * For browser-based login, see AuthViewController.
- */
- @Operation(
- summary = "Log in a user",
- description = "Authenticate with username and password and receive a JWT token"
- )
- @ApiResponses(value = {
- @ApiResponse(
- responseCode = "200",
- description = "Login successful",
- content = @Content(
- mediaType = "application/json",
- schema = @Schema(implementation = LoginResponseDTO.class)
- )
- ),
- @ApiResponse(responseCode = "401", description = "Invalid username or password"),
- @ApiResponse(responseCode = "400", description = "Invalid request body")
- })
- @PostMapping("/login")
- public LoginResponseDTO login(@Valid @RequestBody LoginRequestDTO request) {
+ private final AuthService authService;
+ private final JwtService jwtService;
- User user = authService.login(
- request.getUsername(),
- request.getPassword()
- );
+ public AuthController(AuthService authService, JwtService jwtService) {
+ this.authService = authService;
+ this.jwtService = jwtService;
+ }
- String token = jwtService.generateToken(user);
+ /**
+ * Authenticates a user using JSON input and returns a JWT.
+ *
+ *
This endpoint is mainly used for API testing (e.g. Postman). For browser-based login, see
+ * AuthViewController.
+ */
+ @Operation(
+ summary = "Log in a user",
+ description = "Authenticate with username and password and receive a JWT token")
+ @ApiResponses(
+ value = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "Login successful",
+ content =
+ @Content(
+ mediaType = "application/json",
+ schema = @Schema(implementation = LoginResponseDTO.class))),
+ @ApiResponse(responseCode = "401", description = "Invalid username or password"),
+ @ApiResponse(responseCode = "400", description = "Invalid request body")
+ })
+ @PostMapping("/login")
+ public LoginResponseDTO login(@Valid @RequestBody LoginRequestDTO request) {
- return new LoginResponseDTO(token);
- }
+ User user = authService.login(request.getUsername(), request.getPassword());
+ String token = jwtService.generateToken(user);
- /**
- * Handles user signup by validating input and creating a new account.
- */
- @Operation(
- summary = "Sign up new user",
- description = "Create a new user account with username and password"
- )
- @ApiResponses(value = {
- @ApiResponse(responseCode = "200", description = "User created successfully"),
- @ApiResponse(responseCode = "400", description = "Invalid input"),
- @ApiResponse(responseCode = "409", description = "Username already exists")
- })
- @PostMapping("/signup")
- public void signup(@Valid @RequestBody SignupRequestDTO request) {
- authService.signup(request);
- }
+ return new LoginResponseDTO(token);
+ }
+ /** Handles user signup by validating input and creating a new account. */
+ @Operation(
+ summary = "Sign up new user",
+ description = "Create a new user account with username and password")
+ @ApiResponses(
+ value = {
+ @ApiResponse(responseCode = "200", description = "User created successfully"),
+ @ApiResponse(responseCode = "400", description = "Invalid input"),
+ @ApiResponse(responseCode = "409", description = "Username already exists")
+ })
+ @PostMapping("/signup")
+ public void signup(@Valid @RequestBody SignupRequestDTO request) {
+ authService.signup(request);
+ }
}
diff --git a/src/main/java/org/example/alfs/controllers/AuthViewController.java b/src/main/java/org/example/alfs/controllers/AuthViewController.java
index 5713b82..b828cf3 100644
--- a/src/main/java/org/example/alfs/controllers/AuthViewController.java
+++ b/src/main/java/org/example/alfs/controllers/AuthViewController.java
@@ -1,8 +1,8 @@
package org.example.alfs.controllers;
-import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
+import java.time.Duration;
import org.example.alfs.dto.auth.SignupRequestDTO;
import org.example.alfs.entities.User;
import org.example.alfs.security.JwtService;
@@ -14,103 +14,102 @@
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
-
-import java.time.Duration;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
/**
* Handles login for the browser (UI).
*
- * Uses HTML forms and stores the JWT in a cookie so the user stays logged in.
+ *
Uses HTML forms and stores the JWT in a cookie so the user stays logged in.
*
- * Separate from AuthController, which is used for API login via JSON.
+ *
Separate from AuthController, which is used for API login via JSON.
*/
@Controller
public class AuthViewController {
- private final AuthService authService;
- private final JwtService jwtService;
-
- public AuthViewController(AuthService authService, JwtService jwtService) {
- this.authService = authService;
- this.jwtService = jwtService;
- }
-
- @GetMapping("/login")
- 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")
- public String signupPage() {
- return "signup";
+ private final AuthService authService;
+ private final JwtService jwtService;
+
+ public AuthViewController(AuthService authService, JwtService jwtService) {
+ this.authService = authService;
+ this.jwtService = jwtService;
+ }
+
+ @GetMapping("/login")
+ 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")
+ public String signupPage() {
+ return "signup";
+ }
+
+ @PostMapping("/signup-form")
+ public String signupForm(
+ @Valid @ModelAttribute SignupRequestDTO request, BindingResult bindingResult) {
+ if (bindingResult.hasErrors()) {
+ return "signup";
}
- @PostMapping("/signup-form")
- public String signupForm(
- @Valid @ModelAttribute SignupRequestDTO request,
- BindingResult bindingResult
- ) {
- if (bindingResult.hasErrors()) {
- return "signup";
- }
-
- authService.signup(request);
-
- return "redirect:/login";
+ authService.signup(request);
+ return "redirect:/login";
+ }
- }
-
- @PostMapping("/login-form")
- public String loginForm(
- @RequestParam String username,
- @RequestParam String password,
- HttpServletResponse response,
- RedirectAttributes redirectAttributes
- ) {
- try {
- User user = authService.login(username, password);
+ @PostMapping("/login-form")
+ public String loginForm(
+ @RequestParam String username,
+ @RequestParam String password,
+ HttpServletResponse response,
+ RedirectAttributes redirectAttributes) {
+ try {
+ User user = authService.login(username, password);
- String token = jwtService.generateToken(user);
+ String token = jwtService.generateToken(user);
- ResponseCookie cookie = ResponseCookie.from("JWT", token)
- .httpOnly(true)
- .path("/")
- .maxAge(Duration.ofDays(1))
- .sameSite("Lax")
- .build();
+ ResponseCookie cookie =
+ ResponseCookie.from("JWT", token)
+ .httpOnly(true)
+ .path("/")
+ .maxAge(Duration.ofDays(1))
+ .sameSite("Lax")
+ .build();
- response.addHeader("Set-Cookie", cookie.toString());
+ response.addHeader("Set-Cookie", cookie.toString());
- redirectAttributes.addFlashAttribute("success", "You are signed in!");
- return "redirect:/";
-
- } catch (ResponseStatusException ex) {
-
- // login-error -> redirect to form and show error
- if (ex.getStatusCode() == HttpStatus.UNAUTHORIZED) {
- return "redirect:/login?error=true";
- }
-
- // other error throw
- throw ex;
- }
- }
+ redirectAttributes.addFlashAttribute("success", "You are signed in!");
+ return "redirect:/";
- @PostMapping("/auth/logout")
- public String logout(HttpServletResponse response, RedirectAttributes redirectAttributes) {
+ } catch (ResponseStatusException ex) {
- ResponseCookie cookie = ResponseCookie.from("JWT", "")
- .httpOnly(true)
- .path("/")
- .maxAge(Duration.ZERO)
- .sameSite("Lax")
- .build();
+ // login-error -> redirect to form and show error
+ if (ex.getStatusCode() == HttpStatus.UNAUTHORIZED) {
+ return "redirect:/login?error=true";
+ }
- response.addHeader("Set-Cookie", cookie.toString());
- redirectAttributes.addFlashAttribute("success", "Successfully signed out");
- return "redirect:/";
+ // other error throw
+ throw ex;
}
-}
\ No newline at end of file
+ }
+
+ @PostMapping("/auth/logout")
+ public String logout(HttpServletResponse response, RedirectAttributes redirectAttributes) {
+
+ ResponseCookie cookie =
+ ResponseCookie.from("JWT", "")
+ .httpOnly(true)
+ .path("/")
+ .maxAge(Duration.ZERO)
+ .sameSite("Lax")
+ .build();
+
+ response.addHeader("Set-Cookie", cookie.toString());
+ redirectAttributes.addFlashAttribute("success", "Successfully signed out");
+ return "redirect:/";
+ }
+}
diff --git a/src/main/java/org/example/alfs/controllers/ErrorController.java b/src/main/java/org/example/alfs/controllers/ErrorController.java
index d78b9c4..96aa044 100644
--- a/src/main/java/org/example/alfs/controllers/ErrorController.java
+++ b/src/main/java/org/example/alfs/controllers/ErrorController.java
@@ -10,19 +10,19 @@
@RequestMapping("/error")
public class ErrorController {
- @RequestMapping("/403")
- @ResponseStatus(HttpStatus.FORBIDDEN)
- public String forbidden(Model model) {
- model.addAttribute("status", 403);
- model.addAttribute("error", "Access denied");
- return "error";
- }
+ @RequestMapping("/403")
+ @ResponseStatus(HttpStatus.FORBIDDEN)
+ public String forbidden(Model model) {
+ model.addAttribute("status", 403);
+ model.addAttribute("error", "Access denied");
+ return "error";
+ }
- @RequestMapping("/401")
- @ResponseStatus(HttpStatus.UNAUTHORIZED)
- public String unauthorized(Model model) {
- model.addAttribute("status", 401);
- model.addAttribute("error", "You need to log in to access this page");
- return "error";
- }
+ @RequestMapping("/401")
+ @ResponseStatus(HttpStatus.UNAUTHORIZED)
+ public String unauthorized(Model model) {
+ model.addAttribute("status", 401);
+ model.addAttribute("error", "You need to log in to access this page");
+ return "error";
+ }
}
diff --git a/src/main/java/org/example/alfs/controllers/StartPageController.java b/src/main/java/org/example/alfs/controllers/StartPageController.java
index a41a840..d2fbb77 100644
--- a/src/main/java/org/example/alfs/controllers/StartPageController.java
+++ b/src/main/java/org/example/alfs/controllers/StartPageController.java
@@ -7,8 +7,8 @@
@Controller
public class StartPageController {
- @GetMapping("/")
- public String startPage(Model model){
- return "startPage";
- }
+ @GetMapping("/")
+ public String startPage(Model model) {
+ return "startPage";
+ }
}
diff --git a/src/main/java/org/example/alfs/controllers/TestController.java b/src/main/java/org/example/alfs/controllers/TestController.java
index b7846d7..528157f 100644
--- a/src/main/java/org/example/alfs/controllers/TestController.java
+++ b/src/main/java/org/example/alfs/controllers/TestController.java
@@ -9,56 +9,54 @@
@RestController
public class TestController {
- @PreAuthorize("hasAnyRole('ADMIN','INVESTIGATOR','REPORTER')")
- @GetMapping("/api/test/all-roles")
- public String allRoles() {
- return "all roles allowed";
- }
-
- @PreAuthorize("hasRole('REPORTER')")
- @GetMapping("/api/test/create-ticket")
- public String createTicket() {
- return "reporter can create ticket";
- }
-
-
- @PreAuthorize("hasRole('ADMIN')")
- @GetMapping("/api/test/assign-ticket")
- public String assignTicket() {
- return "admin can assign ticket";
- }
-
- @PreAuthorize("hasRole('INVESTIGATOR')")
- @GetMapping("/api/test/update-status")
- public String updateStatus() {
- return "investigator can update status";
- }
-
- // need to be signed in - all roles
- @GetMapping("/api/hello")
- public String hello() {
- return "hello secured";
- }
-
- // Only ADMIN
- @PreAuthorize("hasRole('ADMIN')")
- @GetMapping("/api/admin/test")
- public String adminOnly() {
- return "only admin";
- }
-
- // Only INVESTIGATOR
- @PreAuthorize("hasRole('INVESTIGATOR')")
- @GetMapping("/api/investigator/test")
- public String investigatorOnly() {
- return "only investigator";
- }
-
- // Only REPORTER
- @PreAuthorize("hasRole('REPORTER')")
- @GetMapping("/api/reporter/test")
- public String reporterOnly() {
- return "only reporter";
- }
-
-}
\ No newline at end of file
+ @PreAuthorize("hasAnyRole('ADMIN','INVESTIGATOR','REPORTER')")
+ @GetMapping("/api/test/all-roles")
+ public String allRoles() {
+ return "all roles allowed";
+ }
+
+ @PreAuthorize("hasRole('REPORTER')")
+ @GetMapping("/api/test/create-ticket")
+ public String createTicket() {
+ return "reporter can create ticket";
+ }
+
+ @PreAuthorize("hasRole('ADMIN')")
+ @GetMapping("/api/test/assign-ticket")
+ public String assignTicket() {
+ return "admin can assign ticket";
+ }
+
+ @PreAuthorize("hasRole('INVESTIGATOR')")
+ @GetMapping("/api/test/update-status")
+ public String updateStatus() {
+ return "investigator can update status";
+ }
+
+ // need to be signed in - all roles
+ @GetMapping("/api/hello")
+ public String hello() {
+ return "hello secured";
+ }
+
+ // Only ADMIN
+ @PreAuthorize("hasRole('ADMIN')")
+ @GetMapping("/api/admin/test")
+ public String adminOnly() {
+ return "only admin";
+ }
+
+ // Only INVESTIGATOR
+ @PreAuthorize("hasRole('INVESTIGATOR')")
+ @GetMapping("/api/investigator/test")
+ public String investigatorOnly() {
+ return "only investigator";
+ }
+
+ // Only REPORTER
+ @PreAuthorize("hasRole('REPORTER')")
+ @GetMapping("/api/reporter/test")
+ public String reporterOnly() {
+ return "only reporter";
+ }
+}
diff --git a/src/main/java/org/example/alfs/controllers/TicketCommentController.java b/src/main/java/org/example/alfs/controllers/TicketCommentController.java
index efd893b..1e770f0 100644
--- a/src/main/java/org/example/alfs/controllers/TicketCommentController.java
+++ b/src/main/java/org/example/alfs/controllers/TicketCommentController.java
@@ -2,11 +2,13 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
+import java.util.List;
import org.example.alfs.dto.comment.CommentCreateDTO;
import org.example.alfs.dto.comment.CommentViewDTO;
import org.example.alfs.entities.User;
@@ -15,86 +17,77 @@
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
-
-import java.util.List;
-
@Controller
@RequestMapping("/tickets")
public class TicketCommentController {
- private final TicketCommentService commentService;
- private final SecurityUtils securityUtils;
-
- public TicketCommentController(TicketCommentService commentService,
- SecurityUtils securityUtils) {
- this.commentService = commentService;
- this.securityUtils = securityUtils;
- }
-
- @PostMapping("/{ticketId}/comments")
- public String addComment(
- @PathVariable Long ticketId,
- @Valid @ModelAttribute CommentCreateDTO dto,
- @RequestParam(required = false) String token
- ) {
- User user = getCurrentUserOrNull();
+ private final TicketCommentService commentService;
+ private final SecurityUtils securityUtils;
- commentService.addComment(ticketId, dto, user, token);
+ public TicketCommentController(TicketCommentService commentService, SecurityUtils securityUtils) {
+ this.commentService = commentService;
+ this.securityUtils = securityUtils;
+ }
- if (token != null && !token.isBlank()) {
- return "redirect:/tickets/token/" + token;
- }
+ @PostMapping("/{ticketId}/comments")
+ public String addComment(
+ @PathVariable Long ticketId,
+ @Valid @ModelAttribute CommentCreateDTO dto,
+ @RequestParam(required = false) String token) {
+ User user = getCurrentUserOrNull();
- return "redirect:/tickets/" + ticketId;
- }
-
- @Operation(
- summary = "Get comments for a ticket",
- description = "Returns all comments for a ticket. Supports both authenticated users and anonymous users via token."
- )
- @ApiResponses(value = {
- @ApiResponse(
- responseCode = "200",
- description = "Comments retrieved successfully",
- content = @Content(
- mediaType = "application/json",
- schema = @Schema(implementation = CommentViewDTO.class)
- )
- ),
- @ApiResponse(responseCode = "403", description = "Access denied"),
- @ApiResponse(responseCode = "404", description = "Ticket not found")
- })
- @GetMapping("/{ticketId}/comments")
- @ResponseBody
- public List getComments(
-
- @Parameter(description = "ID of the ticket", example = "1")
- @PathVariable Long ticketId,
-
- @Parameter(description = "Optional token for anonymous access")
- @RequestParam(required = false) String token
- )
- {
- User user = getCurrentUserOrNull();
+ commentService.addComment(ticketId, dto, user, token);
- return commentService.getComments(ticketId, user, token);
+ if (token != null && !token.isBlank()) {
+ return "redirect:/tickets/token/" + token;
}
- private User getCurrentUserOrNull() {
- try {
- return securityUtils.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;
- }
+ return "redirect:/tickets/" + ticketId;
+ }
+
+ @Operation(
+ summary = "Get comments for a ticket",
+ description =
+ "Returns all comments for a ticket. Supports both authenticated users and anonymous users via token.")
+ @ApiResponses(
+ value = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "Comments retrieved successfully",
+ content =
+ @Content(
+ mediaType = "application/json",
+ array = @ArraySchema(schema = @Schema(implementation = CommentViewDTO.class)))),
+ @ApiResponse(responseCode = "403", description = "Access denied"),
+ @ApiResponse(responseCode = "404", description = "Ticket not found")
+ })
+ @GetMapping("/{ticketId}/comments")
+ @ResponseBody
+ public List getComments(
+ @Parameter(description = "ID of the ticket", example = "1") @PathVariable Long ticketId,
+ @Parameter(description = "Optional token for anonymous access")
+ @RequestParam(required = false)
+ String token) {
+ User user = getCurrentUserOrNull();
+
+ return commentService.getComments(ticketId, user, token);
+ }
+
+ private User getCurrentUserOrNull() {
+ try {
+ return securityUtils.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/controllers/TicketController.java b/src/main/java/org/example/alfs/controllers/TicketController.java
index 561d0aa..391dbc4 100644
--- a/src/main/java/org/example/alfs/controllers/TicketController.java
+++ b/src/main/java/org/example/alfs/controllers/TicketController.java
@@ -1,187 +1,180 @@
package org.example.alfs.controllers;
-import org.example.alfs.security.SecurityUtils;
-import org.example.alfs.services.*;
-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;
import org.example.alfs.dto.ticket.TicketStatusUpdateDTO;
import org.example.alfs.dto.ticket.TicketViewDTO;
+import org.example.alfs.security.SecurityUtils;
+import org.example.alfs.services.*;
+import org.springframework.http.HttpStatus;
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.*;
-
-//TODO: Decide final routes and redirects
+import org.springframework.web.server.ResponseStatusException;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
@RequestMapping("/tickets")
public class TicketController {
- private final TicketService ticketService;
- private final TicketCommentService ticketCommentService;
- private final AttachmentService attachmentService;
- private final AuditService auditService;
- private final SecurityUtils securityUtils;
- private final UserService userService;
-
- public TicketController(TicketService ticketService, TicketCommentService ticketCommentService,AttachmentService attachmentService, AuditService auditService, SecurityUtils securityUtils, UserService userService) {
- this.ticketService = ticketService;
- this.ticketCommentService = ticketCommentService;
- this.attachmentService = attachmentService;
- this.auditService = auditService;
- this.securityUtils = securityUtils;
- this.userService = userService;
+ private final TicketService ticketService;
+ private final TicketCommentService ticketCommentService;
+ private final AttachmentService attachmentService;
+ private final AuditService auditService;
+ private final SecurityUtils securityUtils;
+ private final UserService userService;
+
+ public TicketController(
+ TicketService ticketService,
+ TicketCommentService ticketCommentService,
+ AttachmentService attachmentService,
+ AuditService auditService,
+ SecurityUtils securityUtils,
+ UserService userService) {
+ this.ticketService = ticketService;
+ this.ticketCommentService = ticketCommentService;
+ this.attachmentService = attachmentService;
+ this.auditService = auditService;
+ this.securityUtils = securityUtils;
+ this.userService = userService;
+ }
+
+ // create ticket
+ // @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
+ @PostMapping("/create")
+ public String createNewTicket(
+ @ModelAttribute("ticket") @Valid TicketCreateDTO dto,
+ BindingResult bindingResult,
+ Model model,
+ RedirectAttributes redirectAttributes) {
+ if (bindingResult.hasErrors()) {
+ model.addAttribute("ticket", dto);
+ return "create";
}
- //create ticket
- //@PreAuthorize("hasRole('REPORTER')") // should change later for anonymous access
- @GetMapping("/create")
- public String createNewTicketForm(Model model) {
- model.addAttribute("ticket", new TicketCreateDTO());
- return "create";
- }
+ TicketViewDTO ticket = ticketService.createNewTicket(dto);
+ redirectAttributes.addFlashAttribute("success", "Ticket created successfully");
- //@PreAuthorize("hasRole('REPORTER')") // should change later for anonymous access
- @PostMapping("/create")
- public String createNewTicket(
- @ModelAttribute("ticket") @Valid TicketCreateDTO dto,
- BindingResult bindingResult,
- Model model,
- RedirectAttributes redirectAttributes
- ) {
- if (bindingResult.hasErrors()) {
- model.addAttribute("ticket", dto);
- return "create";
- }
-
- TicketViewDTO ticket = ticketService.createNewTicket(dto);
- redirectAttributes.addFlashAttribute("success", "Ticket created successfully");
-
- if (ticket.getToken() != null) {
- return "redirect:/tickets/ticket-created?token=" + ticket.getToken();
- }
-
- 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
+ @GetMapping("/token/{token}")
+ public String viewTicketByToken(@PathVariable String token, Model model) {
+ try {
+ TicketViewDTO ticket = ticketService.getTicketByToken(token);
- //view ticket by token
- @GetMapping("/token/{token}")
- public String viewTicketByToken(@PathVariable String token, Model model) {
-
- try {
- TicketViewDTO ticket = ticketService.getTicketByToken(token);
+ var user = securityUtils.getCurrentUserOrNull();
- var user = securityUtils.getCurrentUserOrNull();
+ var comments = ticketCommentService.getComments(ticket.getId(), user, token);
+ var attachments = attachmentService.getAttachmentsByTicketId(ticket.getId());
+ var auditLogs = auditService.getAuditLogsForTicket(ticket.getId());
+ var investigators = userService.getAllInvestigators();
- var comments = ticketCommentService.getComments(ticket.getId(), user, token);
- var attachments = attachmentService.getAttachmentsByTicketId(ticket.getId());
- var auditLogs = auditService.getAuditLogsForTicket(ticket.getId());
- var investigators = userService.getAllInvestigators();
+ model.addAttribute("ticket", ticket);
+ model.addAttribute("comments", comments);
+ model.addAttribute("attachments", attachments);
+ model.addAttribute("auditLogs", auditLogs);
+ model.addAttribute("investigators", investigators);
+ model.addAttribute("accessToken", token);
- model.addAttribute("ticket", ticket);
- model.addAttribute("comments", comments);
- model.addAttribute("attachments", attachments);
- model.addAttribute("auditLogs", auditLogs);
- model.addAttribute("investigators", investigators);
- model.addAttribute("accessToken", token);
+ return "view";
- return "view";
+ } catch (ResponseStatusException ex) {
- } catch (ResponseStatusException ex) {
+ if (ex.getStatusCode() == HttpStatus.NOT_FOUND) {
+ return "redirect:/login?tokenError=true";
+ }
- if (ex.getStatusCode() == HttpStatus.NOT_FOUND) {
- return "redirect:/login?tokenError=true";
- }
-
- throw ex;
- }
+ throw ex;
}
+ }
+ // view ticket by id
+ @PreAuthorize("hasAnyRole('ADMIN','INVESTIGATOR','REPORTER')")
+ @GetMapping("/{id}")
+ public String viewTicketById(@PathVariable Long id, Model model) {
- //view ticket by id
- @PreAuthorize("hasAnyRole('ADMIN','INVESTIGATOR','REPORTER')")
- @GetMapping("/{id}")
- public String viewTicketById(@PathVariable Long id, Model model) {
+ TicketViewDTO ticket = ticketService.getTicketById(id);
- TicketViewDTO ticket = ticketService.getTicketById(id);
+ var user = securityUtils.getCurrentUserOrNull();
- var user = securityUtils.getCurrentUserOrNull();
+ // get comments
+ var comments = ticketCommentService.getComments(id, user, null);
- // get comments
- var comments = ticketCommentService.getComments(id, user, null);
+ // get attachments
+ var attachments = attachmentService.getAttachmentsByTicketId(id);
- // get attachments
- var attachments = attachmentService.getAttachmentsByTicketId(id);
+ // get audit logs
+ var auditLogs = auditService.getAuditLogsForTicket(id);
- // get audit logs
- var auditLogs = auditService.getAuditLogsForTicket(id);
+ var investigators = userService.getAllInvestigators();
- var investigators = userService.getAllInvestigators();
+ model.addAttribute("ticket", ticket);
+ model.addAttribute("comments", comments);
+ model.addAttribute("attachments", attachments);
+ model.addAttribute("auditLogs", auditLogs);
+ model.addAttribute("investigators", investigators);
+ model.addAttribute("accessToken", null);
+ return "view";
+ }
- model.addAttribute("ticket", ticket);
- model.addAttribute("comments", comments);
- model.addAttribute("attachments", attachments);
- model.addAttribute("auditLogs", auditLogs);
- model.addAttribute("investigators", investigators);
- model.addAttribute("accessToken", null);
+ // assign ticket
+ @PreAuthorize("hasRole('ADMIN')")
+ @PostMapping("/{id}/assign")
+ public String assignTicket(@PathVariable Long id, @Valid @ModelAttribute TicketAssignDTO dto) {
+ ticketService.assignInvestigator(id, dto.getInvestigatorId());
- return "view";
- }
+ return "redirect:/tickets/" + id;
+ }
+ // update status
+ @PostMapping("/{id}/status")
+ @PreAuthorize("hasAnyRole('ADMIN','INVESTIGATOR')")
+ public String updateStatus(
+ @PathVariable Long id, @Valid @ModelAttribute TicketStatusUpdateDTO dto) {
- //assign ticket
- @PreAuthorize("hasRole('ADMIN')")
- @PostMapping("/{id}/assign")
- public String assignTicket(@PathVariable Long id, @Valid @ModelAttribute TicketAssignDTO dto) {
+ ticketService.updateTicketStatus(id, dto.getStatus());
- ticketService.assignInvestigator(id, dto.getInvestigatorId());
+ return "redirect:/tickets/" + id;
+ }
- return "redirect:/tickets/" + id;
- }
+ @PreAuthorize("hasRole('REPORTER')")
+ @GetMapping("/my")
+ public String myTickets(Model model) {
- //update status
- @PostMapping("/{id}/status")
- @PreAuthorize("hasAnyRole('ADMIN','INVESTIGATOR')")
- public String updateStatus(@PathVariable Long id, @Valid @ModelAttribute TicketStatusUpdateDTO dto) {
+ model.addAttribute("tickets", ticketService.getMyTickets());
+ return "my-tickets";
+ }
- ticketService.updateTicketStatus(id, dto.getStatus());
+ @PreAuthorize("hasRole('INVESTIGATOR')")
+ @GetMapping("/assigned")
+ public String myAssignedTickets(Model model) {
- return "redirect:/tickets/" + id;
- }
+ model.addAttribute("tickets", ticketService.getMyAssignedTickets());
+ return "assigned-tickets";
+ }
-
- @PreAuthorize("hasRole('REPORTER')")
- @GetMapping("/my")
- public String myTickets(Model model) {
-
- model.addAttribute("tickets", ticketService.getMyTickets());
- return "my-tickets";
- }
-
- @PreAuthorize("hasRole('INVESTIGATOR')")
- @GetMapping("/assigned")
- public String myAssignedTickets(Model model) {
-
- model.addAttribute("tickets", ticketService.getMyAssignedTickets());
- 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
+ @GetMapping("/ticket-created")
+ public String ticketCreated(@RequestParam String token, Model model) {
+ model.addAttribute("token", token);
+ return "ticket-created";
+ }
}
diff --git a/src/main/java/org/example/alfs/dto/attachment/AttachmentViewDTO.java b/src/main/java/org/example/alfs/dto/attachment/AttachmentViewDTO.java
index 04c21ca..1d8b9af 100644
--- a/src/main/java/org/example/alfs/dto/attachment/AttachmentViewDTO.java
+++ b/src/main/java/org/example/alfs/dto/attachment/AttachmentViewDTO.java
@@ -3,64 +3,75 @@
import java.time.LocalDateTime;
public class AttachmentViewDTO {
- private Long id;
- private Long ticketId;
- private String fileName;
- private String downloadUrl;
- private LocalDateTime uploadedAt;
- private String uploadedBy;
-
- public AttachmentViewDTO() {}
-
- public AttachmentViewDTO(Long id, Long ticketId, String fileName, String downloadUrl, LocalDateTime uploadedAt, String uploadedBy) {
- this.id = id;
- this.ticketId = ticketId;
- this.fileName = fileName;
- this.downloadUrl = downloadUrl;
- this.uploadedAt = uploadedAt;
- this.uploadedBy = uploadedBy;
- }
-
- public Long getId() {
- return id;
- }
-
- public void setId(Long id) {
- this.id = id;
- }
-
- public Long getTicketId() {
- return ticketId;
- }
-
- public void setTicketId(Long ticketId) {
- this.ticketId = ticketId;
- }
-
- public String getFileName() {
- return fileName;
- }
-
- public void setFileName(String fileName) {
- this.fileName = fileName;
- }
-
- public String getDownloadUrl() {
- return downloadUrl;
- }
-
- public void setDownloadUrl(String downloadUrl) {
- this.downloadUrl = downloadUrl;
- }
-
- public LocalDateTime getUploadedAt() {
- return uploadedAt;
- }
-
- public void setUploadedAt(LocalDateTime uploadedAt) {
- this.uploadedAt = uploadedAt;
- }
-
- public String getUploadedBy() { return uploadedBy; }
- public void setUploadedBy(String uploadedBy) { this.uploadedBy = uploadedBy; }
+ private Long id;
+ private Long ticketId;
+ private String fileName;
+ private String downloadUrl;
+ private LocalDateTime uploadedAt;
+ private String uploadedBy;
+
+ public AttachmentViewDTO() {}
+
+ public AttachmentViewDTO(
+ Long id,
+ Long ticketId,
+ String fileName,
+ String downloadUrl,
+ LocalDateTime uploadedAt,
+ String uploadedBy) {
+ this.id = id;
+ this.ticketId = ticketId;
+ this.fileName = fileName;
+ this.downloadUrl = downloadUrl;
+ this.uploadedAt = uploadedAt;
+ this.uploadedBy = uploadedBy;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public Long getTicketId() {
+ return ticketId;
+ }
+
+ public void setTicketId(Long ticketId) {
+ this.ticketId = ticketId;
+ }
+
+ public String getFileName() {
+ return fileName;
+ }
+
+ public void setFileName(String fileName) {
+ this.fileName = fileName;
+ }
+
+ public String getDownloadUrl() {
+ return downloadUrl;
+ }
+
+ public void setDownloadUrl(String downloadUrl) {
+ this.downloadUrl = downloadUrl;
+ }
+
+ public LocalDateTime getUploadedAt() {
+ return uploadedAt;
+ }
+
+ public void setUploadedAt(LocalDateTime uploadedAt) {
+ this.uploadedAt = uploadedAt;
+ }
+
+ public String getUploadedBy() {
+ return uploadedBy;
+ }
+
+ public void setUploadedBy(String uploadedBy) {
+ this.uploadedBy = uploadedBy;
+ }
}
diff --git a/src/main/java/org/example/alfs/dto/auth/LoginRequestDTO.java b/src/main/java/org/example/alfs/dto/auth/LoginRequestDTO.java
index 522615b..d1b23f1 100644
--- a/src/main/java/org/example/alfs/dto/auth/LoginRequestDTO.java
+++ b/src/main/java/org/example/alfs/dto/auth/LoginRequestDTO.java
@@ -10,12 +10,17 @@
@Schema(description = "Credentials required to authenticate user")
public class LoginRequestDTO {
- @NotBlank(message = "Username is required")
- @Schema(description = "Registered username", example = "cool_username", requiredMode = Schema.RequiredMode.REQUIRED)
- private String username;
-
- @NotBlank(message = "Password is required")
- @Schema(description = "The users secret password", example = "itsAsecret123", requiredMode = Schema.RequiredMode.REQUIRED)
- private String password;
+ @NotBlank(message = "Username is required")
+ @Schema(
+ description = "Registered username",
+ example = "cool_username",
+ requiredMode = Schema.RequiredMode.REQUIRED)
+ private String username;
+ @NotBlank(message = "Password is required")
+ @Schema(
+ description = "The users secret password",
+ example = "itsAsecret123",
+ requiredMode = Schema.RequiredMode.REQUIRED)
+ private String password;
}
diff --git a/src/main/java/org/example/alfs/dto/auth/LoginResponseDTO.java b/src/main/java/org/example/alfs/dto/auth/LoginResponseDTO.java
index f2d2dc5..c527635 100644
--- a/src/main/java/org/example/alfs/dto/auth/LoginResponseDTO.java
+++ b/src/main/java/org/example/alfs/dto/auth/LoginResponseDTO.java
@@ -7,5 +7,5 @@
@AllArgsConstructor
public class LoginResponseDTO {
- private String token;
+ private String token;
}
diff --git a/src/main/java/org/example/alfs/dto/auth/SignupRequestDTO.java b/src/main/java/org/example/alfs/dto/auth/SignupRequestDTO.java
index ada5550..c8bbba2 100644
--- a/src/main/java/org/example/alfs/dto/auth/SignupRequestDTO.java
+++ b/src/main/java/org/example/alfs/dto/auth/SignupRequestDTO.java
@@ -5,18 +5,22 @@
import lombok.Getter;
import lombok.Setter;
-
@Getter
@Setter
@Schema(description = "Credentials required to sign up new user")
public class SignupRequestDTO {
- @NotBlank(message = "Username is required")
- @Schema(description = "New username", example = "cool_username", requiredMode = Schema.RequiredMode.REQUIRED)
- private String username;
-
- @NotBlank(message = "Password is required")
- @Schema(description = "New password", example = "itsAsecret123", requiredMode = Schema.RequiredMode.REQUIRED)
- private String password;
+ @NotBlank(message = "Username is required")
+ @Schema(
+ description = "New username",
+ example = "cool_username",
+ requiredMode = Schema.RequiredMode.REQUIRED)
+ private String username;
+ @NotBlank(message = "Password is required")
+ @Schema(
+ description = "New password",
+ example = "itsAsecret123",
+ requiredMode = Schema.RequiredMode.REQUIRED)
+ private String password;
}
diff --git a/src/main/java/org/example/alfs/dto/comment/CommentCreateDTO.java b/src/main/java/org/example/alfs/dto/comment/CommentCreateDTO.java
index 8b2ed19..6322230 100644
--- a/src/main/java/org/example/alfs/dto/comment/CommentCreateDTO.java
+++ b/src/main/java/org/example/alfs/dto/comment/CommentCreateDTO.java
@@ -10,9 +10,7 @@
@Data
public class CommentCreateDTO {
- @NotBlank
- private String message;
+ @NotBlank private String message;
- private boolean internalNote = false;
-
-}
\ No newline at end of file
+ private boolean internalNote = false;
+}
diff --git a/src/main/java/org/example/alfs/dto/comment/CommentViewDTO.java b/src/main/java/org/example/alfs/dto/comment/CommentViewDTO.java
index 2b063db..f729978 100644
--- a/src/main/java/org/example/alfs/dto/comment/CommentViewDTO.java
+++ b/src/main/java/org/example/alfs/dto/comment/CommentViewDTO.java
@@ -1,8 +1,7 @@
package org.example.alfs.dto.comment;
-import lombok.Data;
-
import java.time.LocalDateTime;
+import lombok.Data;
/*
* DTO returned when retrieving comments for a ticket.
@@ -17,19 +16,16 @@
@Data
public class CommentViewDTO {
- private Long id;
- private String message;
- private String author;
- private String role;
- private LocalDateTime createdAt;
-
- private boolean internalNote;
+ private Long id;
+ private String message;
+ private String author;
+ private String role;
+ private LocalDateTime createdAt;
+ private boolean internalNote;
- public String getFormattedCreatedAt() {
- if (createdAt == null) return "";
- return createdAt.format(
- java.time.format.DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm")
- );
- }
+ public String getFormattedCreatedAt() {
+ if (createdAt == null) return "";
+ return createdAt.format(java.time.format.DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm"));
+ }
}
diff --git a/src/main/java/org/example/alfs/dto/ticket/TicketAssignDTO.java b/src/main/java/org/example/alfs/dto/ticket/TicketAssignDTO.java
index 037089b..ba732f1 100644
--- a/src/main/java/org/example/alfs/dto/ticket/TicketAssignDTO.java
+++ b/src/main/java/org/example/alfs/dto/ticket/TicketAssignDTO.java
@@ -9,6 +9,5 @@
@Data
public class TicketAssignDTO {
- @NotNull
- private Long investigatorId;
+ @NotNull private Long investigatorId;
}
diff --git a/src/main/java/org/example/alfs/dto/ticket/TicketCreateDTO.java b/src/main/java/org/example/alfs/dto/ticket/TicketCreateDTO.java
index 568f0ed..457cbeb 100644
--- a/src/main/java/org/example/alfs/dto/ticket/TicketCreateDTO.java
+++ b/src/main/java/org/example/alfs/dto/ticket/TicketCreateDTO.java
@@ -14,10 +14,7 @@
@Data
public class TicketCreateDTO {
+ @NotBlank private String title;
- @NotBlank
- private String title;
-
- @NotBlank
- private String description;
+ @NotBlank private String description;
}
diff --git a/src/main/java/org/example/alfs/dto/ticket/TicketStatusUpdateDTO.java b/src/main/java/org/example/alfs/dto/ticket/TicketStatusUpdateDTO.java
index c75cfd9..45df824 100644
--- a/src/main/java/org/example/alfs/dto/ticket/TicketStatusUpdateDTO.java
+++ b/src/main/java/org/example/alfs/dto/ticket/TicketStatusUpdateDTO.java
@@ -5,15 +5,12 @@
import org.example.alfs.enums.TicketStatus;
/*
-*Investigator changes tickets status
-*The status represents the lifecycle of the ticket
-* (e.g. OPEN, IN_PROGRESS, RESOLVED, CLOSED).
-*/
+ *Investigator changes tickets status
+ *The status represents the lifecycle of the ticket
+ * (e.g. OPEN, IN_PROGRESS, RESOLVED, CLOSED).
+ */
@Data
public class TicketStatusUpdateDTO {
- @NotNull
- private TicketStatus status;
-
-
+ @NotNull private TicketStatus status;
}
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 495517a..cbfe88e 100644
--- a/src/main/java/org/example/alfs/dto/ticket/TicketViewDTO.java
+++ b/src/main/java/org/example/alfs/dto/ticket/TicketViewDTO.java
@@ -1,10 +1,10 @@
package org.example.alfs.dto.ticket;
-import lombok.Data;
-import org.example.alfs.enums.TicketStatus;
-import java.time.format.DateTimeFormatter;
import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
import java.util.Locale;
+import lombok.Data;
+import org.example.alfs.enums.TicketStatus;
/*
* DTO returned when retrieving ticket information.
@@ -17,22 +17,22 @@
@Data
public class TicketViewDTO {
- private Long id;
- private String token;
- private String title;
- private String description;
- private TicketStatus status;
- private LocalDateTime createdAt;
+ private Long id;
+ private String token;
+ private String title;
+ private String description;
+ private TicketStatus status;
+ private LocalDateTime createdAt;
- private Long assignedInvestigatorId;
- private String assignedInvestigatorName;
+ private Long assignedInvestigatorId;
+ private String assignedInvestigatorName;
- private static final DateTimeFormatter DISPLAY_FORMATTER =
- DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm", Locale.forLanguageTag("sv-SE"));
+ private static final DateTimeFormatter DISPLAY_FORMATTER =
+ DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm", Locale.forLanguageTag("sv-SE"));
- public String getFormattedCreatedAt() {
- if (createdAt == null) return "";
+ public String getFormattedCreatedAt() {
+ if (createdAt == null) return "";
- return createdAt.format(DISPLAY_FORMATTER);
- }
+ return createdAt.format(DISPLAY_FORMATTER);
+ }
}
diff --git a/src/main/java/org/example/alfs/entities/Attachment.java b/src/main/java/org/example/alfs/entities/Attachment.java
index c4c13e6..94cdcd1 100644
--- a/src/main/java/org/example/alfs/entities/Attachment.java
+++ b/src/main/java/org/example/alfs/entities/Attachment.java
@@ -1,9 +1,8 @@
package org.example.alfs.entities;
import jakarta.persistence.*;
-import lombok.*;
-
import java.time.LocalDateTime;
+import lombok.*;
/*
Represent a file uploaded with Ticket.
@@ -17,28 +16,28 @@
@NoArgsConstructor
public class Attachment {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
- @Column(nullable = false)
- private String fileName;
+ @Column(nullable = false)
+ private String fileName;
- @Column(nullable = false)
- private String s3Key;
+ @Column(nullable = false)
+ private String s3Key;
- private LocalDateTime uploadedAt;
+ private LocalDateTime uploadedAt;
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "uploaded_by")
- private User uploadedBy;
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "uploaded_by")
+ private User uploadedBy;
- @PrePersist
- public void prePersist() {
- uploadedAt = LocalDateTime.now();
- }
+ @PrePersist
+ public void prePersist() {
+ uploadedAt = LocalDateTime.now();
+ }
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "ticket_id", nullable = false)
- private Ticket ticket;
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "ticket_id", nullable = false)
+ private Ticket ticket;
}
diff --git a/src/main/java/org/example/alfs/entities/AuditLog.java b/src/main/java/org/example/alfs/entities/AuditLog.java
index 9d00090..e8e8c90 100644
--- a/src/main/java/org/example/alfs/entities/AuditLog.java
+++ b/src/main/java/org/example/alfs/entities/AuditLog.java
@@ -1,11 +1,10 @@
package org.example.alfs.entities;
import jakarta.persistence.*;
+import java.time.LocalDateTime;
import lombok.*;
import org.example.alfs.enums.AuditAction;
-import java.time.LocalDateTime;
-
/*
Represents audit log for Ticket.
Logs all events such as status change, assignment and comments.
@@ -19,43 +18,41 @@
@AllArgsConstructor
public class AuditLog {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
- @Enumerated(EnumType.STRING)
- @Column(nullable = false, length = 50)
- private AuditAction action;
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false, length = 50)
+ private AuditAction action;
- @Column(nullable = false)
- private String fieldName;
+ @Column(nullable = false)
+ private String fieldName;
- @Column(length = 4000)
- private String oldValue;
+ @Column(length = 4000)
+ private String oldValue;
- @Column(length = 4000)
- private String newValue;
+ @Column(length = 4000)
+ private String newValue;
- @Column(nullable = false)
- private LocalDateTime createdAt;
+ @Column(nullable = false)
+ private LocalDateTime createdAt;
- @PrePersist
- public void prePersist() {
- createdAt = LocalDateTime.now();
- }
+ @PrePersist
+ public void prePersist() {
+ createdAt = LocalDateTime.now();
+ }
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "ticket_id")
- private Ticket ticket;
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "ticket_id")
+ private Ticket ticket;
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "user_id")
- private User user;
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "user_id")
+ private User user;
- public String getFormattedCreatedAt() {
- if (createdAt == null) return "";
- return createdAt.format(
- java.time.format.DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm")
- );
- }
+ public String getFormattedCreatedAt() {
+ if (createdAt == null) return "";
+ return createdAt.format(java.time.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 6db2207..29c4c25 100644
--- a/src/main/java/org/example/alfs/entities/Ticket.java
+++ b/src/main/java/org/example/alfs/entities/Ticket.java
@@ -1,12 +1,10 @@
package org.example.alfs.entities;
import jakarta.persistence.*;
-import lombok.*;
-import org.example.alfs.enums.TicketStatus;
-
import java.time.LocalDateTime;
import java.util.List;
-import java.util.UUID;
+import lombok.*;
+import org.example.alfs.enums.TicketStatus;
/*
Representing a whistleblower report.
@@ -21,53 +19,53 @@
@NoArgsConstructor
public class Ticket {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
- @Column(nullable = false, length = 255, updatable = false)
- private String title;
+ @Column(nullable = false, length = 255, updatable = false)
+ private String title;
- @Basic(fetch = FetchType.LAZY)
- @Column(nullable = false, columnDefinition = "TEXT", updatable = false)
- private String description;
+ @Basic(fetch = FetchType.LAZY)
+ @Column(nullable = false, columnDefinition = "TEXT", updatable = false)
+ private String description;
- @Enumerated(EnumType.STRING)
- @Column(nullable = false, length = 32)
- private TicketStatus status;
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false, length = 32)
+ private TicketStatus status;
- @Column(nullable = true, unique = true, length = 128, updatable = false)
- private String reporterToken;
+ @Column(nullable = true, unique = true, length = 128, updatable = false)
+ private String reporterToken;
- private LocalDateTime createdAt;
+ private LocalDateTime createdAt;
- private LocalDateTime updatedAt;
+ private LocalDateTime updatedAt;
- @PrePersist
- public void prePersist() {
- createdAt = LocalDateTime.now();
- if (status == null) status = TicketStatus.OPEN;
- }
+ @PrePersist
+ public void prePersist() {
+ createdAt = LocalDateTime.now();
+ if (status == null) status = TicketStatus.OPEN;
+ }
- @PreUpdate
- public void preUpdate() {
- updatedAt = LocalDateTime.now();
- }
+ @PreUpdate
+ public void preUpdate() {
+ updatedAt = LocalDateTime.now();
+ }
- @OneToMany(mappedBy = "ticket")
- private List comments;
+ @OneToMany(mappedBy = "ticket")
+ private List comments;
- @OneToMany(mappedBy = "ticket")
- private List attachments;
+ @OneToMany(mappedBy = "ticket")
+ private List attachments;
- @OneToMany(mappedBy = "ticket")
- private List auditLogs;
+ @OneToMany(mappedBy = "ticket")
+ private List auditLogs;
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "reporter_id", nullable = true) // null if anonymous
- private User reporter;
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "reporter_id", nullable = true) // null if anonymous
+ private User reporter;
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "investigator_id", nullable = true)
- private User investigator;
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "investigator_id", nullable = true)
+ private User investigator;
}
diff --git a/src/main/java/org/example/alfs/entities/TicketComment.java b/src/main/java/org/example/alfs/entities/TicketComment.java
index 48e3727..79cb0a2 100644
--- a/src/main/java/org/example/alfs/entities/TicketComment.java
+++ b/src/main/java/org/example/alfs/entities/TicketComment.java
@@ -1,10 +1,8 @@
package org.example.alfs.entities;
import jakarta.persistence.*;
-import lombok.*;
-
-
import java.time.LocalDateTime;
+import lombok.*;
/*
Represents comment on a ticket.
@@ -18,30 +16,30 @@
@NoArgsConstructor
public class TicketComment {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
- @Basic(fetch = FetchType.LAZY)
- @Column(nullable = false, columnDefinition = "TEXT")
- private String message;
+ @Basic(fetch = FetchType.LAZY)
+ @Column(nullable = false, columnDefinition = "TEXT")
+ private String message;
- // Möjlighet att skriva internt meddelande (synligt för utredare/admin, inte för anmälare)
- @Column(nullable = false)
- private boolean internalNote = false;
+ // Möjlighet att skriva internt meddelande (synligt för utredare/admin, inte för anmälare)
+ @Column(nullable = false)
+ private boolean internalNote = false;
- private LocalDateTime createdAt;
+ private LocalDateTime createdAt;
- @PrePersist
- public void prePersist() {
- createdAt = LocalDateTime.now();
- }
+ @PrePersist
+ public void prePersist() {
+ createdAt = LocalDateTime.now();
+ }
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "ticket_id", nullable = false)
- private Ticket ticket;
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "ticket_id", nullable = false)
+ private Ticket ticket;
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "author_id", nullable = true)
- private User author;
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "author_id", nullable = true)
+ private User author;
}
diff --git a/src/main/java/org/example/alfs/entities/User.java b/src/main/java/org/example/alfs/entities/User.java
index 0319601..136af2c 100644
--- a/src/main/java/org/example/alfs/entities/User.java
+++ b/src/main/java/org/example/alfs/entities/User.java
@@ -16,25 +16,24 @@
@NoArgsConstructor
public class User {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
- @Column(nullable = false, unique = true, length = 100)
- private String username;
+ @Column(nullable = false, unique = true, length = 100)
+ private String username;
- @Column(nullable = false, length = 255)
- private String passwordHash;
+ @Column(nullable = false, length = 255)
+ private String passwordHash;
- @Enumerated(EnumType.STRING)
- @Column(nullable = false)
- private Role role;
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private Role role;
- @PrePersist
- public void prePersist() {
- if (role == null) {
- role = Role.REPORTER;
- }
+ @PrePersist
+ public void prePersist() {
+ if (role == null) {
+ role = Role.REPORTER;
}
-
+ }
}
diff --git a/src/main/java/org/example/alfs/enums/AuditAction.java b/src/main/java/org/example/alfs/enums/AuditAction.java
index 44f2fd3..8d4c7a4 100644
--- a/src/main/java/org/example/alfs/enums/AuditAction.java
+++ b/src/main/java/org/example/alfs/enums/AuditAction.java
@@ -1,10 +1,10 @@
package org.example.alfs.enums;
public enum AuditAction {
- CREATED,
- STATUS_CHANGED,
- ASSIGNED,
- UNASSIGNED,
- COMMENT_ADDED,
- ATTACHMENT_ADDED
-}
\ No newline at end of file
+ CREATED,
+ STATUS_CHANGED,
+ ASSIGNED,
+ UNASSIGNED,
+ COMMENT_ADDED,
+ ATTACHMENT_ADDED
+}
diff --git a/src/main/java/org/example/alfs/enums/Role.java b/src/main/java/org/example/alfs/enums/Role.java
index 3024fa1..090fd64 100644
--- a/src/main/java/org/example/alfs/enums/Role.java
+++ b/src/main/java/org/example/alfs/enums/Role.java
@@ -1,7 +1,7 @@
package org.example.alfs.enums;
public enum Role {
- REPORTER,
- INVESTIGATOR,
- ADMIN
-}
\ No newline at end of file
+ REPORTER,
+ INVESTIGATOR,
+ ADMIN
+}
diff --git a/src/main/java/org/example/alfs/enums/TicketStatus.java b/src/main/java/org/example/alfs/enums/TicketStatus.java
index 6442277..2179095 100644
--- a/src/main/java/org/example/alfs/enums/TicketStatus.java
+++ b/src/main/java/org/example/alfs/enums/TicketStatus.java
@@ -1,8 +1,8 @@
package org.example.alfs.enums;
public enum TicketStatus {
- OPEN,
- IN_PROGRESS,
- RESOLVED,
- CLOSED
+ OPEN,
+ IN_PROGRESS,
+ RESOLVED,
+ CLOSED
}
diff --git a/src/main/java/org/example/alfs/exceptions/GlobalExceptionHandler.java b/src/main/java/org/example/alfs/exceptions/GlobalExceptionHandler.java
index 9a92124..9a8e5d5 100644
--- a/src/main/java/org/example/alfs/exceptions/GlobalExceptionHandler.java
+++ b/src/main/java/org/example/alfs/exceptions/GlobalExceptionHandler.java
@@ -9,34 +9,27 @@
@ControllerAdvice
public class GlobalExceptionHandler {
- @ExceptionHandler(ResponseStatusException.class)
- public String handleResponseStatusException(
- ResponseStatusException ex,
- Model model,
- HttpServletResponse response
- ) {
+ @ExceptionHandler(ResponseStatusException.class)
+ public String handleResponseStatusException(
+ ResponseStatusException ex, Model model, HttpServletResponse response) {
- int status = ex.getStatusCode().value();
- response.setStatus(status);
+ int status = ex.getStatusCode().value();
+ response.setStatus(status);
- model.addAttribute("status", status);
- model.addAttribute("error", ex.getReason());
+ model.addAttribute("status", status);
+ model.addAttribute("error", ex.getReason());
- return "error";
- }
+ return "error";
+ }
- @ExceptionHandler(Exception.class)
- public String handleException(
- Exception ex,
- Model model,
- HttpServletResponse response
- ) {
+ @ExceptionHandler(Exception.class)
+ public String handleException(Exception ex, Model model, HttpServletResponse response) {
- response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
- model.addAttribute("status", 500);
- model.addAttribute("error", "Something went wrong");
+ model.addAttribute("status", 500);
+ model.addAttribute("error", "Something went wrong");
- return "error";
- }
-}
\ No newline at end of file
+ return "error";
+ }
+}
diff --git a/src/main/java/org/example/alfs/mapper/TicketCommentMapper.java b/src/main/java/org/example/alfs/mapper/TicketCommentMapper.java
index 7fee2c5..ec1838f 100644
--- a/src/main/java/org/example/alfs/mapper/TicketCommentMapper.java
+++ b/src/main/java/org/example/alfs/mapper/TicketCommentMapper.java
@@ -6,22 +6,22 @@
@Component
public class TicketCommentMapper {
- public CommentViewDTO entityToViewDTO(TicketComment comment) {
- CommentViewDTO dto = new CommentViewDTO();
+ public CommentViewDTO entityToViewDTO(TicketComment comment) {
+ CommentViewDTO dto = new CommentViewDTO();
- dto.setId(comment.getId());
- dto.setMessage(comment.getMessage());
- dto.setCreatedAt(comment.getCreatedAt());
- dto.setInternalNote(comment.isInternalNote());
+ dto.setId(comment.getId());
+ dto.setMessage(comment.getMessage());
+ dto.setCreatedAt(comment.getCreatedAt());
+ dto.setInternalNote(comment.isInternalNote());
- if (comment.getAuthor() != null) {
- dto.setAuthor(comment.getAuthor().getUsername());
- dto.setRole(comment.getAuthor().getRole().name());
- } else {
- dto.setAuthor("Anonymous");
- dto.setRole(null);
- }
-
- return dto;
+ if (comment.getAuthor() != null) {
+ dto.setAuthor(comment.getAuthor().getUsername());
+ dto.setRole(comment.getAuthor().getRole().name());
+ } else {
+ dto.setAuthor("Anonymous");
+ dto.setRole(null);
}
+
+ return dto;
+ }
}
diff --git a/src/main/java/org/example/alfs/mapper/TicketMapper.java b/src/main/java/org/example/alfs/mapper/TicketMapper.java
index 5de4831..3f8dd32 100644
--- a/src/main/java/org/example/alfs/mapper/TicketMapper.java
+++ b/src/main/java/org/example/alfs/mapper/TicketMapper.java
@@ -7,23 +7,21 @@
@Component
public class TicketMapper {
- public TicketViewDTO entityToViewDTO(Ticket ticket){
+ public TicketViewDTO entityToViewDTO(Ticket ticket) {
- TicketViewDTO ticketViewDTO = new TicketViewDTO();
+ TicketViewDTO ticketViewDTO = new TicketViewDTO();
- ticketViewDTO.setId(ticket.getId());
- ticketViewDTO.setTitle(ticket.getTitle());
- ticketViewDTO.setDescription(ticket.getDescription());
- ticketViewDTO.setStatus(ticket.getStatus());
- ticketViewDTO.setCreatedAt(ticket.getCreatedAt());
+ ticketViewDTO.setId(ticket.getId());
+ ticketViewDTO.setTitle(ticket.getTitle());
+ ticketViewDTO.setDescription(ticket.getDescription());
+ ticketViewDTO.setStatus(ticket.getStatus());
+ ticketViewDTO.setCreatedAt(ticket.getCreatedAt());
- if (ticket.getInvestigator() != null) {
- ticketViewDTO.setAssignedInvestigatorId(ticket.getInvestigator().getId());
- ticketViewDTO.setAssignedInvestigatorName(ticket.getInvestigator().getUsername());
- }
-
- return ticketViewDTO;
+ if (ticket.getInvestigator() != null) {
+ ticketViewDTO.setAssignedInvestigatorId(ticket.getInvestigator().getId());
+ ticketViewDTO.setAssignedInvestigatorName(ticket.getInvestigator().getUsername());
}
-
+ return ticketViewDTO;
+ }
}
diff --git a/src/main/java/org/example/alfs/repositories/AttachmentRepository.java b/src/main/java/org/example/alfs/repositories/AttachmentRepository.java
index e777d6d..c8ab070 100644
--- a/src/main/java/org/example/alfs/repositories/AttachmentRepository.java
+++ b/src/main/java/org/example/alfs/repositories/AttachmentRepository.java
@@ -1,13 +1,11 @@
package org.example.alfs.repositories;
+import java.util.List;
import org.example.alfs.entities.Attachment;
import org.springframework.data.jpa.repository.JpaRepository;
-import java.util.List;
-
public interface AttachmentRepository extends JpaRepository {
- // Hämta alla bilagor i ett fall
- List findByTicketId(Long ticketId);
-
+ // Hämta alla bilagor i ett fall
+ List findByTicketId(Long ticketId);
}
diff --git a/src/main/java/org/example/alfs/repositories/AuditLogRepository.java b/src/main/java/org/example/alfs/repositories/AuditLogRepository.java
index b211ce3..31dd951 100644
--- a/src/main/java/org/example/alfs/repositories/AuditLogRepository.java
+++ b/src/main/java/org/example/alfs/repositories/AuditLogRepository.java
@@ -1,13 +1,11 @@
package org.example.alfs.repositories;
+import java.util.List;
import org.example.alfs.entities.AuditLog;
import org.springframework.data.jpa.repository.JpaRepository;
-import java.util.List;
-
public interface AuditLogRepository extends JpaRepository {
- // Hämta logghistoriken i ett fall, nyast först
- List findByTicketIdOrderByCreatedAtDesc(Long ticketId);
-
+ // Hämta logghistoriken i ett fall, nyast först
+ List findByTicketIdOrderByCreatedAtDesc(Long ticketId);
}
diff --git a/src/main/java/org/example/alfs/repositories/TicketCommentRepository.java b/src/main/java/org/example/alfs/repositories/TicketCommentRepository.java
index 44730b3..6d228dd 100644
--- a/src/main/java/org/example/alfs/repositories/TicketCommentRepository.java
+++ b/src/main/java/org/example/alfs/repositories/TicketCommentRepository.java
@@ -1,18 +1,18 @@
package org.example.alfs.repositories;
+import java.util.List;
import org.example.alfs.entities.TicketComment;
import org.springframework.data.jpa.repository.JpaRepository;
-import java.util.List;
-
public interface TicketCommentRepository extends JpaRepository {
- // Ladda alla kommentarer i ett fall, äldst först
- List findByTicketIdOrderByCreatedAtAsc(Long ticketId);
+ // Ladda alla kommentarer i ett fall, äldst först
+ List findByTicketIdOrderByCreatedAtAsc(Long ticketId);
- // Ladda interna meddelanden för utredare/admins, äldst först
- List findByTicketIdAndInternalNoteOrderByCreatedAtAsc(Long ticketId, boolean isInternalNote);
+ // Ladda interna meddelanden för utredare/admins, äldst först
+ List findByTicketIdAndInternalNoteOrderByCreatedAtAsc(
+ Long ticketId, boolean isInternalNote);
- // Ladda kommentarer men utelämna interna meddelanden, äldst först
- List findByTicketIdAndInternalNoteFalseOrderByCreatedAtAsc(Long ticketId);
+ // Ladda kommentarer men utelämna interna meddelanden, äldst först
+ List findByTicketIdAndInternalNoteFalseOrderByCreatedAtAsc(Long ticketId);
}
diff --git a/src/main/java/org/example/alfs/repositories/TicketRepository.java b/src/main/java/org/example/alfs/repositories/TicketRepository.java
index 2aad7a1..edbf033 100644
--- a/src/main/java/org/example/alfs/repositories/TicketRepository.java
+++ b/src/main/java/org/example/alfs/repositories/TicketRepository.java
@@ -1,33 +1,30 @@
package org.example.alfs.repositories;
+import java.util.List;
+import java.util.Optional;
import org.example.alfs.entities.Ticket;
import org.example.alfs.enums.TicketStatus;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
-import java.util.List;
-import java.util.Optional;
-
public interface TicketRepository extends JpaRepository {
- // Anonym anmälare ser sitt fall med token
- Optional findByReporterToken(String reporterToken);
-
- // Inloggad anmälare ser sitt/sina fall
- List findByReporterId(Long reporterId);
-
- // Utredare ser sina tilldelade fall
- List findByInvestigatorId(Long investigatorId);
+ // Anonym anmälare ser sitt fall med token
+ Optional findByReporterToken(String reporterToken);
- // Filtrera fall efter status
- List findByStatus(TicketStatus status);
+ // Inloggad anmälare ser sitt/sina fall
+ List findByReporterId(Long reporterId);
- // Filtrera fall efter status och utredare
- List findByStatusAndInvestigatorId(TicketStatus status, Long investigatorId);
+ // Utredare ser sina tilldelade fall
+ List findByInvestigatorId(Long investigatorId);
- // Hämta alla fall, paginerat
- Page findAll(Pageable pageable);
+ // Filtrera fall efter status
+ List findByStatus(TicketStatus status);
+ // Filtrera fall efter status och utredare
+ List findByStatusAndInvestigatorId(TicketStatus status, Long investigatorId);
+ // Hämta alla fall, paginerat
+ Page findAll(Pageable pageable);
}
diff --git a/src/main/java/org/example/alfs/repositories/UserRepository.java b/src/main/java/org/example/alfs/repositories/UserRepository.java
index 6614902..2383d90 100644
--- a/src/main/java/org/example/alfs/repositories/UserRepository.java
+++ b/src/main/java/org/example/alfs/repositories/UserRepository.java
@@ -1,17 +1,16 @@
package org.example.alfs.repositories;
+import java.util.List;
+import java.util.Optional;
import org.example.alfs.entities.User;
import org.example.alfs.enums.Role;
import org.springframework.data.jpa.repository.JpaRepository;
-import java.util.List;
-import java.util.Optional;
-
public interface UserRepository extends JpaRepository {
- Optional findByUsername(String username);
+ Optional findByUsername(String username);
+
+ boolean existsByUsername(String username);
- boolean existsByUsername(String username);
-
- List findByRole(Role role);
+ List findByRole(Role role);
}
diff --git a/src/main/java/org/example/alfs/security/JwtAuthenticationFilter.java b/src/main/java/org/example/alfs/security/JwtAuthenticationFilter.java
index 3559b73..ed56d6d 100644
--- a/src/main/java/org/example/alfs/security/JwtAuthenticationFilter.java
+++ b/src/main/java/org/example/alfs/security/JwtAuthenticationFilter.java
@@ -2,8 +2,11 @@
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.List;
import org.example.alfs.entities.User;
import org.example.alfs.repositories.UserRepository;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -11,114 +14,108 @@
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
-import jakarta.servlet.http.Cookie;
-import java.io.IOException;
-import java.util.List;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
- private final JwtService jwtService;
- private final UserRepository userRepository;
-
- public JwtAuthenticationFilter(JwtService jwtService, UserRepository userRepository) {
- this.jwtService = jwtService;
- this.userRepository = userRepository;
+ private final JwtService jwtService;
+ private final UserRepository userRepository;
+
+ public JwtAuthenticationFilter(JwtService jwtService, UserRepository userRepository) {
+ this.jwtService = jwtService;
+ this.userRepository = userRepository;
+ }
+
+ // 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")
+ || path.startsWith("/startPage")
+
+ // swagger
+ || path.startsWith("/swagger-ui")
+ || path.startsWith("/v3/api-docs");
+ }
+
+ @Override
+ protected void doFilterInternal(
+ HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+ throws ServletException, IOException {
+
+ final String authHeader = request.getHeader("Authorization");
+
+ String jwt = null;
+ // Try to read JWT from Authorization header (used by API/Postman)
+ // Ignore empty Bearer tokens so cookie fallback still works
+ if (authHeader != null && authHeader.startsWith("Bearer ")) {
+ String bearer = authHeader.substring(7).trim();
+ if (!bearer.isEmpty()) {
+ jwt = bearer;
+ }
}
- // 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") || path.startsWith("/startPage")
-
- //swagger
- || path.startsWith("/swagger-ui")
- || path.startsWith("/v3/api-docs");
- }
-
-
- @Override
- protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
-
- final String authHeader = request.getHeader("Authorization");
-
- String jwt = null;
- // Try to read JWT from Authorization header (used by API/Postman)
- // Ignore empty Bearer tokens so cookie fallback still works
- if(authHeader != null && authHeader.startsWith("Bearer ")){
- String bearer = authHeader.substring(7).trim();
- if (!bearer.isEmpty()) {
- jwt = bearer;
- }
- }
-
- // If no valid token in header, try reading JWT from cookies (browser)
- if ((jwt == null || jwt.isBlank()) && request.getCookies() != null) {
- for (Cookie cookie : request.getCookies()) {
- if ("JWT".equals(cookie.getName())) {
- jwt = cookie.getValue();
- break;
- }
- }
+ // If no valid token in header, try reading JWT from cookies (browser)
+ if ((jwt == null || jwt.isBlank()) && request.getCookies() != null) {
+ for (Cookie cookie : request.getCookies()) {
+ if ("JWT".equals(cookie.getName())) {
+ jwt = cookie.getValue();
+ break;
}
+ }
+ }
- // if still no token, continue without auth
- if(jwt == null) {
- filterChain.doFilter(request, response);
- return;
- }
+ // if still no token, continue without auth
+ if (jwt == null) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+ String username;
+ try {
+ username = jwtService.extractUsername(jwt);
+ } catch (Exception e) {
+ filterChain.doFilter(request, response);
+ return;
+ }
- String username;
- try {
- username = jwtService.extractUsername(jwt);
- } catch (Exception e) {
- filterChain.doFilter(request, response);
- return;
- }
+ if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
- if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
-
- User user = userRepository.findByUsername(username).orElse(null);
-
- if (user == null) {
- filterChain.doFilter(request, response);
- return;
- }
-
- // IMPORTANT:
- // We do NOT trust the role stored in the JWT.
- // Instead, we always load the user's role from the database.
- //
- // This ensures that if a user's role changes (e.g. ADMIN → REPORTER),
- // the change takes effect immediately, even if the old JWT is still valid.
- org.springframework.security.core.userdetails.UserDetails userDetails =
- new org.springframework.security.core.userdetails.User(
- user.getUsername(),
- user.getPasswordHash(),
- List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole().name()))
- );
-
- boolean valid = jwtService.isTokenValid(jwt, user);
-
- if (valid) {
- UsernamePasswordAuthenticationToken authToken =
- new UsernamePasswordAuthenticationToken(
- userDetails,
- null,
- userDetails.getAuthorities()
- );
-
- authToken.setDetails(
- new org.springframework.security.web.authentication.WebAuthenticationDetailsSource()
- .buildDetails(request)
- );
-
- SecurityContextHolder.getContext().setAuthentication(authToken);
- }
- }
+ User user = userRepository.findByUsername(username).orElse(null);
+ if (user == null) {
filterChain.doFilter(request, response);
+ return;
+ }
+
+ // IMPORTANT:
+ // We do NOT trust the role stored in the JWT.
+ // Instead, we always load the user's role from the database.
+ //
+ // This ensures that if a user's role changes (e.g. ADMIN → REPORTER),
+ // the change takes effect immediately, even if the old JWT is still valid.
+ org.springframework.security.core.userdetails.UserDetails userDetails =
+ new org.springframework.security.core.userdetails.User(
+ user.getUsername(),
+ user.getPasswordHash(),
+ List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole().name())));
+
+ boolean valid = jwtService.isTokenValid(jwt, user);
+
+ if (valid) {
+ UsernamePasswordAuthenticationToken authToken =
+ new UsernamePasswordAuthenticationToken(
+ userDetails, null, userDetails.getAuthorities());
+
+ authToken.setDetails(
+ new org.springframework.security.web.authentication.WebAuthenticationDetailsSource()
+ .buildDetails(request));
+
+ SecurityContextHolder.getContext().setAuthentication(authToken);
+ }
}
-}
\ No newline at end of file
+
+ filterChain.doFilter(request, response);
+ }
+}
diff --git a/src/main/java/org/example/alfs/security/JwtService.java b/src/main/java/org/example/alfs/security/JwtService.java
index c5d6938..31cd024 100644
--- a/src/main/java/org/example/alfs/security/JwtService.java
+++ b/src/main/java/org/example/alfs/security/JwtService.java
@@ -4,88 +4,82 @@
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
-import org.example.alfs.entities.User;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Service;
-
-import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
+import javax.crypto.SecretKey;
+import org.example.alfs.entities.User;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
@Service
public class JwtService {
- @Value("${jwt.secret}")
- private String secretKey;
-
- @Value("${jwt.expiration}")
- private long jwtExpiration;
-
- public String generateToken(User user) {
- Map claims = new HashMap<>();
-
-
- // NOTE:
- // We include the user's role in the JWT for potential future use.
- // However, the application does NOT use the role from the token for authorization.
- //
- // Instead, the user's role is always fetched from the database.
- // This ensures that any changes to user permissions take effect immediately,
- // without waiting for the JWT to expire.
- //
- // This design prioritizes security and consistency over performance.
- claims.put("role", user.getRole().name());
-
- return buildToken(claims, user.getUsername());
- }
-
- private String buildToken(Map claims, String username) {
- return Jwts.builder()
- .claims(claims)
- .subject(username)
- .issuedAt(new Date(System.currentTimeMillis()))
- .expiration(new Date(System.currentTimeMillis() + jwtExpiration))
- .signWith(getSigningKey())
- .compact();
- }
-
- public String extractUsername(String token) {
- return extractClaim(token, Claims::getSubject);
- }
-
- public String extractRole(String token) {
- return extractAllClaims(token).get("role", String.class);
- }
-
- public boolean isTokenValid(String token, User user) {
- final String username = extractUsername(token);
- return username.equals(user.getUsername()) && !isTokenExpired(token);
- }
-
- public boolean isTokenExpired(String token) {
- return extractExpiration(token).before(new Date());
- }
-
- private Claims extractAllClaims(String token) {
- return Jwts.parser()
- .verifyWith(getSigningKey())
- .build()
- .parseSignedClaims(token)
- .getPayload();
- }
-
- private SecretKey getSigningKey() {
- byte[] keyBytes = Decoders.BASE64.decode(secretKey);
- return Keys.hmacShaKeyFor(keyBytes);
- }
-
- private Date extractExpiration(String token) {
- return extractClaim(token, Claims::getExpiration);
- }
-
- private T extractClaim(String token, java.util.function.Function claimsResolver) {
- Claims claims = extractAllClaims(token);
- return claimsResolver.apply(claims);
- }
+ @Value("${jwt.secret}")
+ private String secretKey;
+
+ @Value("${jwt.expiration}")
+ private long jwtExpiration;
+
+ public String generateToken(User user) {
+ Map claims = new HashMap<>();
+
+ // NOTE:
+ // We include the user's role in the JWT for potential future use.
+ // However, the application does NOT use the role from the token for authorization.
+ //
+ // Instead, the user's role is always fetched from the database.
+ // This ensures that any changes to user permissions take effect immediately,
+ // without waiting for the JWT to expire.
+ //
+ // This design prioritizes security and consistency over performance.
+ claims.put("role", user.getRole().name());
+
+ return buildToken(claims, user.getUsername());
+ }
+
+ private String buildToken(Map claims, String username) {
+ return Jwts.builder()
+ .claims(claims)
+ .subject(username)
+ .issuedAt(new Date(System.currentTimeMillis()))
+ .expiration(new Date(System.currentTimeMillis() + jwtExpiration))
+ .signWith(getSigningKey())
+ .compact();
+ }
+
+ public String extractUsername(String token) {
+ return extractClaim(token, Claims::getSubject);
+ }
+
+ public String extractRole(String token) {
+ return extractAllClaims(token).get("role", String.class);
+ }
+
+ public boolean isTokenValid(String token, User user) {
+ final String username = extractUsername(token);
+ return username.equals(user.getUsername()) && !isTokenExpired(token);
+ }
+
+ public boolean isTokenExpired(String token) {
+ return extractExpiration(token).before(new Date());
+ }
+
+ private Claims extractAllClaims(String token) {
+ return Jwts.parser().verifyWith(getSigningKey()).build().parseSignedClaims(token).getPayload();
+ }
+
+ private SecretKey getSigningKey() {
+ byte[] keyBytes = Decoders.BASE64.decode(secretKey);
+ return Keys.hmacShaKeyFor(keyBytes);
+ }
+
+ private Date extractExpiration(String token) {
+ return extractClaim(token, Claims::getExpiration);
+ }
+
+ private T extractClaim(String token, java.util.function.Function claimsResolver) {
+ Claims claims = extractAllClaims(token);
+ return claimsResolver.apply(claims);
+ }
}
diff --git a/src/main/java/org/example/alfs/security/SecurityUtils.java b/src/main/java/org/example/alfs/security/SecurityUtils.java
index 9d76be1..94674fa 100644
--- a/src/main/java/org/example/alfs/security/SecurityUtils.java
+++ b/src/main/java/org/example/alfs/security/SecurityUtils.java
@@ -8,42 +8,43 @@
@Component
public class SecurityUtils {
- private final UserRepository userRepository;
+ private final UserRepository userRepository;
- public SecurityUtils(UserRepository userRepository) {
- this.userRepository = userRepository;
- }
+ public SecurityUtils(UserRepository userRepository) {
+ this.userRepository = userRepository;
+ }
- public User getCurrentUser() {
+ public User getCurrentUser() {
- var authentication = SecurityContextHolder.getContext().getAuthentication();
+ var authentication = SecurityContextHolder.getContext().getAuthentication();
- if (authentication == null || !authentication.isAuthenticated()) {
- throw new RuntimeException("No authenticated user in security context");
- }
+ if (authentication == null || !authentication.isAuthenticated()) {
+ throw new RuntimeException("No authenticated user in security context");
+ }
- String username = authentication.getName();
+ String username = authentication.getName();
- return userRepository.findByUsername(username)
- .orElseThrow(() -> new RuntimeException("Authenticated user not found in database"));
- }
+ return userRepository
+ .findByUsername(username)
+ .orElseThrow(() -> new RuntimeException("Authenticated user not found in database"));
+ }
- public User getCurrentUserOrNull() {
- try {
- return getCurrentUser();
- } catch (RuntimeException ex) {
+ public User getCurrentUserOrNull() {
+ try {
+ return getCurrentUser();
+ } catch (RuntimeException ex) {
- String message = ex.getMessage();
+ String message = ex.getMessage();
- boolean authFailure =
- "No authenticated user in security context".equals(message) ||
- "Authenticated user not found in database".equals(message);
+ boolean authFailure =
+ "No authenticated user in security context".equals(message)
+ || "Authenticated user not found in database".equals(message);
- if (authFailure) {
- return null;
- }
+ if (authFailure) {
+ return null;
+ }
- throw ex;
- }
+ throw ex;
}
-}
\ No newline at end of file
+ }
+}
diff --git a/src/main/java/org/example/alfs/services/AttachmentService.java b/src/main/java/org/example/alfs/services/AttachmentService.java
index c986a14..c964ff9 100644
--- a/src/main/java/org/example/alfs/services/AttachmentService.java
+++ b/src/main/java/org/example/alfs/services/AttachmentService.java
@@ -1,5 +1,6 @@
package org.example.alfs.services;
+import java.util.List;
import org.example.alfs.entities.Attachment;
import org.example.alfs.entities.Ticket;
import org.example.alfs.entities.User;
@@ -14,128 +15,125 @@
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
-import java.util.List;
-
@Service
public class AttachmentService {
- private final MinioStorageService storageService;
- private final AttachmentRepository attachmentRepository;
- private final TicketRepository ticketRepository;
- private final AuditService auditService;
+ private final MinioStorageService storageService;
+ private final AttachmentRepository attachmentRepository;
+ private final TicketRepository ticketRepository;
+ private final AuditService auditService;
+
+ public AttachmentService(
+ MinioStorageService storageService,
+ AttachmentRepository attachmentRepository,
+ TicketRepository ticketRepository,
+ AuditService auditService) {
+ this.storageService = storageService;
+ this.attachmentRepository = attachmentRepository;
+ this.ticketRepository = ticketRepository;
+ this.auditService = auditService;
+ }
+
+ @Transactional
+ public Attachment uploadToTicket(Long ticketId, MultipartFile file, User user, String token)
+ throws Exception {
+
+ Ticket ticket;
+
+ if (user != null) {
+ // logged in
+ ticket =
+ ticketRepository
+ .findById(ticketId)
+ .orElseThrow(
+ () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found"));
+ } else {
+
+ // validate token first
+ if (token == null || token.isBlank()) {
+ throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid or missing token");
+ }
+
+ // anonymous via token
+ ticket =
+ ticketRepository
+ .findByReporterToken(token)
+ .orElseThrow(
+ () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found"));
+ }
+
+ checkAccess(ticket, user, token);
+ String objectKey = storageService.upload(file);
- public AttachmentService(MinioStorageService storageService,
- AttachmentRepository attachmentRepository,
- TicketRepository ticketRepository,
- AuditService auditService) {
- this.storageService = storageService;
- this.attachmentRepository = attachmentRepository;
- this.ticketRepository = ticketRepository;
- this.auditService = auditService;
- }
+ try {
+ String fileName = file.getOriginalFilename();
+ if (fileName == null || fileName.isBlank()) {
+ fileName = "file";
+ }
+
+ Attachment att = new Attachment();
+ att.setFileName(fileName);
+ att.setS3Key(objectKey);
+ att.setTicket(ticket);
+ att.setUploadedBy(user);
+
+ attachmentRepository.save(att);
- @Transactional
- public Attachment uploadToTicket(Long ticketId, MultipartFile file, User user, String token) throws Exception {
-
- Ticket ticket;
-
- if (user != null) {
- // logged in
- ticket = ticketRepository.findById(ticketId)
- .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found"));
- } else {
-
- // validate token first
- if (token == null || token.isBlank()) {
- throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid or missing token");
- }
-
- // anonymous via token
- ticket = ticketRepository.findByReporterToken(token)
- .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found"));
- }
-
- checkAccess(ticket, user, token);
-
- String objectKey = storageService.upload(file);
-
- try {
- String fileName = file.getOriginalFilename();
- if (fileName == null || fileName.isBlank()) {
- fileName = "file";
- }
-
- Attachment att = new Attachment();
- att.setFileName(fileName);
- att.setS3Key(objectKey);
- att.setTicket(ticket);
- att.setUploadedBy(user);
-
- attachmentRepository.save(att);
-
- auditService.log(
- AuditAction.ATTACHMENT_ADDED,
- "attachments",
- null,
- att.getFileName(),
- ticket,
- user
- );
-
- return att;
-
- } catch (Exception e) {
- try {
- storageService.delete(objectKey);
- } catch (Exception deleteEx) {
- e.addSuppressed(deleteEx);
- }
- throw e;
- }
+ auditService.log(
+ AuditAction.ATTACHMENT_ADDED, "attachments", null, att.getFileName(), ticket, user);
+
+ return att;
+
+ } catch (Exception e) {
+ try {
+ storageService.delete(objectKey);
+ } catch (Exception deleteEx) {
+ e.addSuppressed(deleteEx);
+ }
+ throw e;
}
+ }
- private void checkAccess(Ticket ticket, User user, String token) {
-
- // ANONYMOUS VIA TOKEN
- if (user == null) {
- if (token != null && token.equals(ticket.getReporterToken())) {
- return;
- }
- throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid or missing token");
- }
-
- // ADMIN
- if (user.getRole() == Role.ADMIN) return;
-
- // INVESTIGATOR
- if (user.getRole() == Role.INVESTIGATOR) {
- if (ticket.getInvestigator() != null &&
- ticket.getInvestigator().getId().equals(user.getId())) {
- return;
- }
- }
-
- // REPORTER
- if (user.getRole() == Role.REPORTER) {
- if (ticket.getReporter() != null &&
- ticket.getReporter().getId().equals(user.getId())) {
- return;
- }
- }
-
- throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Access denied");
+ private void checkAccess(Ticket ticket, User user, String token) {
+
+ // ANONYMOUS VIA TOKEN
+ if (user == null) {
+ if (token != null && token.equals(ticket.getReporterToken())) {
+ return;
+ }
+ throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid or missing token");
}
- public List getAttachmentsByTicketId(Long ticketId) {
- return attachmentRepository.findByTicketId(ticketId);
+ // ADMIN
+ if (user.getRole() == Role.ADMIN) return;
+
+ // INVESTIGATOR
+ if (user.getRole() == Role.INVESTIGATOR) {
+ if (ticket.getInvestigator() != null
+ && ticket.getInvestigator().getId().equals(user.getId())) {
+ return;
+ }
}
- public Attachment getAttachmentById(Long id) {
- return attachmentRepository.findById(id)
- .orElseThrow(() -> new ResponseStatusException(
- HttpStatus.NOT_FOUND,
- "Attachment not found: " + id
- ));
+ // REPORTER
+ if (user.getRole() == Role.REPORTER) {
+ if (ticket.getReporter() != null && ticket.getReporter().getId().equals(user.getId())) {
+ return;
+ }
}
+
+ throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Access denied");
+ }
+
+ public List getAttachmentsByTicketId(Long ticketId) {
+ return attachmentRepository.findByTicketId(ticketId);
+ }
+
+ public Attachment getAttachmentById(Long id) {
+ return attachmentRepository
+ .findById(id)
+ .orElseThrow(
+ () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Attachment not found: " + id));
+ }
}
diff --git a/src/main/java/org/example/alfs/services/AuditService.java b/src/main/java/org/example/alfs/services/AuditService.java
index f32f9e4..5e11068 100644
--- a/src/main/java/org/example/alfs/services/AuditService.java
+++ b/src/main/java/org/example/alfs/services/AuditService.java
@@ -1,5 +1,6 @@
package org.example.alfs.services;
+import java.util.List;
import org.example.alfs.entities.AuditLog;
import org.example.alfs.entities.Ticket;
import org.example.alfs.entities.User;
@@ -7,43 +8,47 @@
import org.example.alfs.repositories.AuditLogRepository;
import org.springframework.stereotype.Service;
-import java.util.List;
-
@Service
public class AuditService {
- private final AuditLogRepository auditLogRepository;
-
- public AuditService(AuditLogRepository auditLogRepository) {
- this.auditLogRepository = auditLogRepository;
- }
-
-
- // new with user
- public void log(AuditAction action, String fieldName, String oldValue, String newValue, Ticket ticket, User user) {
- AuditLog log = new AuditLog();
- log.setAction(action);
- log.setFieldName(fieldName);
- log.setOldValue(oldValue);
- log.setNewValue(newValue);
- log.setTicket(ticket);
- log.setUser(user);
- auditLogRepository.save(log);
- }
-
- // keeping old for safety
- public void log(AuditAction action, String fieldName, String oldValue, String newValue, Ticket ticket) {
- AuditLog log = new AuditLog();
- log.setAction(action);
- log.setFieldName(fieldName);
- log.setOldValue(oldValue);
- log.setNewValue(newValue);
- log.setTicket(ticket);
- // createdAt sätts automatiskt via @PrePersist i AuditLog
- auditLogRepository.save(log);
- }
-
- public List getAuditLogsForTicket(Long ticketId) {
- return auditLogRepository.findByTicketIdOrderByCreatedAtDesc(ticketId);
- }
+ private final AuditLogRepository auditLogRepository;
+
+ public AuditService(AuditLogRepository auditLogRepository) {
+ this.auditLogRepository = auditLogRepository;
+ }
+
+ // new with user
+ public void log(
+ AuditAction action,
+ String fieldName,
+ String oldValue,
+ String newValue,
+ Ticket ticket,
+ User user) {
+ AuditLog log = new AuditLog();
+ log.setAction(action);
+ log.setFieldName(fieldName);
+ log.setOldValue(oldValue);
+ log.setNewValue(newValue);
+ log.setTicket(ticket);
+ log.setUser(user);
+ auditLogRepository.save(log);
+ }
+
+ // keeping old for safety
+ public void log(
+ AuditAction action, String fieldName, String oldValue, String newValue, Ticket ticket) {
+ AuditLog log = new AuditLog();
+ log.setAction(action);
+ log.setFieldName(fieldName);
+ log.setOldValue(oldValue);
+ log.setNewValue(newValue);
+ log.setTicket(ticket);
+ // createdAt sätts automatiskt via @PrePersist i AuditLog
+ auditLogRepository.save(log);
+ }
+
+ public List getAuditLogsForTicket(Long ticketId) {
+ return auditLogRepository.findByTicketIdOrderByCreatedAtDesc(ticketId);
+ }
}
diff --git a/src/main/java/org/example/alfs/services/AuthService.java b/src/main/java/org/example/alfs/services/AuthService.java
index a8cec36..f871d90 100644
--- a/src/main/java/org/example/alfs/services/AuthService.java
+++ b/src/main/java/org/example/alfs/services/AuthService.java
@@ -12,47 +12,45 @@
@Service
public class AuthService {
- private final PasswordEncoder passwordEncoder;
- private final UserRepository userRepository;
+ private final PasswordEncoder passwordEncoder;
+ private final UserRepository userRepository;
- public AuthService(PasswordEncoder passwordEncoder, UserRepository userRepository) {
- this.passwordEncoder = passwordEncoder;
- this.userRepository = userRepository;
- }
-
- /**
- * Authenticates a user by verifying username and password.
- */
- public User login(String username, String password) {
-
- User user = userRepository.findByUsername(username)
- .orElseThrow(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Bad credentials"));
+ public AuthService(PasswordEncoder passwordEncoder, UserRepository userRepository) {
+ this.passwordEncoder = passwordEncoder;
+ this.userRepository = userRepository;
+ }
+ /** Authenticates a user by verifying username and password. */
+ public User login(String username, String password) {
+ User user =
+ userRepository
+ .findByUsername(username)
+ .orElseThrow(
+ () -> new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Bad credentials"));
- if (!passwordEncoder.matches(password, user.getPasswordHash())) {
- throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Bad credentials");
- }
-
- return user;
+ if (!passwordEncoder.matches(password, user.getPasswordHash())) {
+ throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Bad credentials");
}
+ return user;
+ }
- /**
- * Registers a new user by creating an account with a hashed password.
- * The user is assigned the default role REPORTER.
- */
- public void signup(SignupRequestDTO request) {
+ /**
+ * Registers a new user by creating an account with a hashed password. The user is assigned the
+ * default role REPORTER.
+ */
+ public void signup(SignupRequestDTO request) {
- if (userRepository.findByUsername(request.getUsername()).isPresent()) {
- throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Username already exists");
- }
+ if (userRepository.findByUsername(request.getUsername()).isPresent()) {
+ throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Username already exists");
+ }
- User user = new User();
- user.setUsername(request.getUsername());
- user.setPasswordHash(passwordEncoder.encode(request.getPassword()));
- user.setRole(Role.REPORTER);
+ User user = new User();
+ user.setUsername(request.getUsername());
+ user.setPasswordHash(passwordEncoder.encode(request.getPassword()));
+ user.setRole(Role.REPORTER);
- userRepository.save(user);
- }
+ userRepository.save(user);
+ }
}
diff --git a/src/main/java/org/example/alfs/services/DemoDataService.java b/src/main/java/org/example/alfs/services/DemoDataService.java
index 9efcc7a..d75f54d 100644
--- a/src/main/java/org/example/alfs/services/DemoDataService.java
+++ b/src/main/java/org/example/alfs/services/DemoDataService.java
@@ -1,5 +1,7 @@
package org.example.alfs.services;
+import java.util.List;
+import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.alfs.entities.*;
@@ -11,227 +13,199 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
-import java.util.List;
-import java.util.UUID;
-
@Service
@Slf4j
@RequiredArgsConstructor
public class DemoDataService {
- private final UserRepository userRepository;
- private final TicketRepository ticketRepository;
- private final TicketCommentRepository commentRepository;
- private final AttachmentRepository attachmentRepository;
- private final AuditLogRepository auditLogRepository;
- private final PasswordEncoder passwordEncoder;
-
- @Transactional
- public void seedDemoData() {
- if (userRepository.existsByUsername("admin")) {
- log.info("Database already contains data, skipping demo data seeding.");
- return;
- }
-
- log.info("Seeding demo data...");
-
- SeedUsers users = createUsers();
- SeedTickets tickets = createTickets(users);
- createComments(tickets, users);
- createAttachments(tickets, users);
-
- log.info("Demo data seeding completed.");
+ private final UserRepository userRepository;
+ private final TicketRepository ticketRepository;
+ private final TicketCommentRepository commentRepository;
+ private final AttachmentRepository attachmentRepository;
+ private final AuditLogRepository auditLogRepository;
+ private final PasswordEncoder passwordEncoder;
+
+ @Transactional
+ public void seedDemoData() {
+ if (userRepository.existsByUsername("admin")) {
+ log.info("Database already contains data, skipping demo data seeding.");
+ return;
}
- private SeedUsers createUsers() {
- User admin = new User();
- admin.setUsername("admin");
- admin.setPasswordHash(passwordEncoder.encode("admin"));
- admin.setRole(Role.ADMIN);
-
- User investigator1 = new User();
- investigator1.setUsername("investigator1");
- investigator1.setPasswordHash(passwordEncoder.encode("investigator1"));
- investigator1.setRole(Role.INVESTIGATOR);
-
- User investigator2 = new User();
- investigator2.setUsername("investigator2");
- investigator2.setPasswordHash(passwordEncoder.encode("investigator2"));
- investigator2.setRole(Role.INVESTIGATOR);
-
- User reporter1 = new User();
- reporter1.setUsername("reporter1");
- reporter1.setPasswordHash(passwordEncoder.encode("reporter1"));
- reporter1.setRole(Role.REPORTER);
-
- User reporter2 = new User();
- reporter2.setUsername("reporter2");
- reporter2.setPasswordHash(passwordEncoder.encode("reporter2"));
- reporter2.setRole(Role.REPORTER);
-
- userRepository.saveAll(List.of(
- admin,
- investigator1,
- investigator2,
- reporter1,
- reporter2
- ));
-
- return new SeedUsers(admin, investigator1, investigator2, reporter1, reporter2);
+ log.info("Seeding demo data...");
+
+ SeedUsers users = createUsers();
+ SeedTickets tickets = createTickets(users);
+ createComments(tickets, users);
+ createAttachments(tickets, users);
+
+ log.info("Demo data seeding completed.");
+ }
+
+ private SeedUsers createUsers() {
+ User admin = new User();
+ admin.setUsername("admin");
+ admin.setPasswordHash(passwordEncoder.encode("admin"));
+ admin.setRole(Role.ADMIN);
+
+ User investigator1 = new User();
+ investigator1.setUsername("investigator1");
+ investigator1.setPasswordHash(passwordEncoder.encode("investigator1"));
+ investigator1.setRole(Role.INVESTIGATOR);
+
+ User investigator2 = new User();
+ investigator2.setUsername("investigator2");
+ investigator2.setPasswordHash(passwordEncoder.encode("investigator2"));
+ investigator2.setRole(Role.INVESTIGATOR);
+
+ User reporter1 = new User();
+ reporter1.setUsername("reporter1");
+ reporter1.setPasswordHash(passwordEncoder.encode("reporter1"));
+ reporter1.setRole(Role.REPORTER);
+
+ User reporter2 = new User();
+ reporter2.setUsername("reporter2");
+ reporter2.setPasswordHash(passwordEncoder.encode("reporter2"));
+ reporter2.setRole(Role.REPORTER);
+
+ userRepository.saveAll(List.of(admin, investigator1, investigator2, reporter1, reporter2));
+
+ return new SeedUsers(admin, investigator1, investigator2, reporter1, reporter2);
+ }
+
+ private record SeedUsers(
+ User admin, User investigator1, User investigator2, User reporter1, User reporter2) {}
+
+ private SeedTickets createTickets(SeedUsers u) {
+ Ticket t1 =
+ addTicket(
+ "Corruption case", "Procurement issue", u.reporter1(), u.investigator1(), u.admin());
+ changeStatus(t1, u.investigator1(), TicketStatus.IN_PROGRESS);
+
+ Ticket t2 =
+ addTicket(
+ "Anonymous harassment",
+ "Ongoing issue",
+ null, // Anonymous reporter
+ u.investigator2(),
+ u.admin());
+ changeStatus(t2, u.investigator2(), TicketStatus.IN_PROGRESS);
+
+ Ticket t3 =
+ addTicket(
+ "Financial misreporting",
+ "Accounting irregularities",
+ u.reporter2(),
+ u.investigator1(),
+ u.admin());
+ changeStatus(t3, u.investigator1(), TicketStatus.IN_PROGRESS);
+ changeStatus(t3, u.investigator1(), TicketStatus.RESOLVED);
+
+ Ticket t4 = addTicket("Unauthorized access", "Security issue", u.reporter1(), null, u.admin());
+
+ return new SeedTickets(t1, t2, t3, t4);
+ }
+
+ private record SeedTickets(Ticket t1, Ticket t2, Ticket t3, Ticket t4) {}
+
+ private Ticket addTicket(
+ String title, String description, User reporter, User investigator, User admin) {
+ Ticket t = new Ticket();
+
+ t.setTitle(title);
+ t.setDescription(description);
+ t.setStatus(TicketStatus.OPEN);
+ t.setReporter(reporter);
+ t.setInvestigator(investigator);
+
+ if (reporter == null) {
+ t.setReporterToken(UUID.randomUUID().toString());
}
- private record SeedUsers(
- User admin,
- User investigator1,
- User investigator2,
- User reporter1,
- User reporter2
- ) {
- }
+ t = ticketRepository.save(t);
- private SeedTickets createTickets(SeedUsers u) {
- Ticket t1 = addTicket(
- "Corruption case",
- "Procurement issue",
- u.reporter1(),
- u.investigator1(),
- u.admin()
- );
- changeStatus(t1, u.investigator1(), TicketStatus.IN_PROGRESS);
-
- Ticket t2 = addTicket(
- "Anonymous harassment",
- "Ongoing issue",
- null, // Anonymous reporter
- u.investigator2(),
- u.admin()
- );
- changeStatus(t2, u.investigator2(), TicketStatus.IN_PROGRESS);
-
- Ticket t3 = addTicket(
- "Financial misreporting",
- "Accounting irregularities",
- u.reporter2(),
- u.investigator1(),
- u.admin()
- );
- changeStatus(t3, u.investigator1(), TicketStatus.IN_PROGRESS);
- changeStatus(t3, u.investigator1(), TicketStatus.RESOLVED);
-
- Ticket t4 = addTicket(
- "Unauthorized access",
- "Security issue",
- u.reporter1(),
- null,
- u.admin()
- );
-
- return new SeedTickets(t1, t2, t3, t4);
+ if (reporter == null) {
+ addAuditLog(t, null, AuditAction.CREATED, "ticket", null, "created (anonymous)");
+ } else {
+ addAuditLog(t, reporter, AuditAction.CREATED, "ticket", null, "created");
}
- private record SeedTickets(
- Ticket t1,
- Ticket t2,
- Ticket t3,
- Ticket t4
- ) {
+ if (investigator != null) {
+ addAuditLog(t, admin, AuditAction.ASSIGNED, "investigator", null, investigator.getUsername());
}
- private Ticket addTicket(String title, String description, User reporter, User investigator, User admin) {
- Ticket t = new Ticket();
+ return t;
+ }
- t.setTitle(title);
- t.setDescription(description);
- t.setStatus(TicketStatus.OPEN);
- t.setReporter(reporter);
- t.setInvestigator(investigator);
+ private void createComments(SeedTickets t, SeedUsers u) {
+ addComment(t.t1(), u.reporter1(), "Can someone look into this ASAP?");
+ addComment(t.t1(), u.investigator1(), "We are investigating this matter.");
+ addComment(t.t1(), u.reporter1(), "Thank you for the update.");
- if (reporter == null) {
- t.setReporterToken(UUID.randomUUID().toString());
- }
+ addComment(t.t2(), null, "This is ongoing for months.");
+ addComment(t.t2(), u.investigator2(), "We take this seriously and will escalate.");
- t = ticketRepository.save(t);
+ addComment(t.t3(), u.investigator1(), "Issue identified and resolved.");
- if (reporter == null) {
- addAuditLog(t, null, AuditAction.CREATED, "ticket", null, "created (anonymous)");
- } else {
- addAuditLog(t, reporter, AuditAction.CREATED, "ticket", null, "created");
- }
+ addComment(t.t4(), u.reporter1(), "No response yet.");
+ }
- if (investigator != null) {
- addAuditLog(t, admin, AuditAction.ASSIGNED, "investigator", null, investigator.getUsername());
- }
+ private void addComment(Ticket ticket, User author, String message) {
+ TicketComment c = new TicketComment();
- return t;
- }
+ c.setTicket(ticket);
+ c.setAuthor(author);
+ c.setMessage(message);
+ c.setInternalNote(false);
- private void createComments(SeedTickets t, SeedUsers u) {
- addComment(t.t1(), u.reporter1(), "Can someone look into this ASAP?");
- addComment(t.t1(), u.investigator1(), "We are investigating this matter.");
- addComment(t.t1(), u.reporter1(), "Thank you for the update.");
+ commentRepository.save(c);
- addComment(t.t2(), null, "This is ongoing for months.");
- addComment(t.t2(), u.investigator2(), "We take this seriously and will escalate.");
+ addAuditLog(ticket, author, AuditAction.COMMENT_ADDED, "comment", null, message);
+ }
- addComment(t.t3(), u.investigator1(), "Issue identified and resolved.");
+ private void createAttachments(SeedTickets t, SeedUsers u) {
+ addAttachment(t.t1(), "procurement-doc.pdf", u.reporter1());
- addComment(t.t4(), u.reporter1(), "No response yet.");
- }
+ addAttachment(t.t2(), "complaint-evidence.txt", null);
- private void addComment(Ticket ticket, User author, String message) {
- TicketComment c = new TicketComment();
+ addAttachment(t.t3(), "financial-report.xlsx", u.investigator1());
+ }
- c.setTicket(ticket);
- c.setAuthor(author);
- c.setMessage(message);
- c.setInternalNote(false);
+ private void addAttachment(Ticket ticket, String fileName, User actor) {
+ Attachment a = new Attachment();
- commentRepository.save(c);
+ a.setTicket(ticket);
+ a.setFileName(fileName);
+ a.setS3Key("demo/" + UUID.randomUUID());
+ a.setUploadedBy(actor);
- addAuditLog(ticket, author, AuditAction.COMMENT_ADDED, "comment", null, message);
- }
-
- private void createAttachments(SeedTickets t, SeedUsers u) {
- addAttachment(t.t1(), "procurement-doc.pdf", u.reporter1());
-
- addAttachment(t.t2(), "complaint-evidence.txt", null);
+ attachmentRepository.save(a);
- addAttachment(t.t3(), "financial-report.xlsx", u.investigator1());
- }
+ addAuditLog(ticket, actor, AuditAction.ATTACHMENT_ADDED, "attachment", null, fileName);
+ }
- private void addAttachment(Ticket ticket, String fileName, User actor) {
- Attachment a = new Attachment();
+ private void addAuditLog(
+ Ticket ticket, User user, AuditAction action, String field, String oldVal, String newVal) {
+ AuditLog entry = new AuditLog();
- a.setTicket(ticket);
- a.setFileName(fileName);
- a.setS3Key("demo/" + UUID.randomUUID());
+ entry.setTicket(ticket);
+ entry.setUser(user);
+ entry.setAction(action);
+ entry.setFieldName(field);
+ entry.setOldValue(oldVal);
+ entry.setNewValue(newVal);
- attachmentRepository.save(a);
+ auditLogRepository.save(entry);
+ }
- addAuditLog(ticket, actor, AuditAction.ATTACHMENT_ADDED, "attachment", null, fileName);
- }
+ private void changeStatus(Ticket t, User user, TicketStatus newStatus) {
+ String oldStatus = t.getStatus().name();
- private void addAuditLog(Ticket ticket, User user, AuditAction action, String field, String oldVal, String newVal) {
- AuditLog entry = new AuditLog();
+ if (t.getStatus() == newStatus) return;
- entry.setTicket(ticket);
- entry.setUser(user);
- entry.setAction(action);
- entry.setFieldName(field);
- entry.setOldValue(oldVal);
- entry.setNewValue(newVal);
+ t.setStatus(newStatus);
+ ticketRepository.save(t);
- auditLogRepository.save(entry);
- }
-
- private void changeStatus(Ticket t, User user, TicketStatus newStatus) {
- String oldStatus = t.getStatus().name();
-
- if (t.getStatus() == newStatus) return;
-
- t.setStatus(newStatus);
- ticketRepository.save(t);
-
- addAuditLog(t, user, AuditAction.STATUS_CHANGED, "status", oldStatus, newStatus.name());
- }
+ addAuditLog(t, user, AuditAction.STATUS_CHANGED, "status", oldStatus, newStatus.name());
+ }
}
diff --git a/src/main/java/org/example/alfs/services/TicketCommentService.java b/src/main/java/org/example/alfs/services/TicketCommentService.java
index 1cc1c95..78879d1 100644
--- a/src/main/java/org/example/alfs/services/TicketCommentService.java
+++ b/src/main/java/org/example/alfs/services/TicketCommentService.java
@@ -1,5 +1,6 @@
package org.example.alfs.services;
+import java.util.List;
import org.example.alfs.dto.comment.CommentCreateDTO;
import org.example.alfs.dto.comment.CommentViewDTO;
import org.example.alfs.entities.Ticket;
@@ -14,97 +15,102 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
-import java.util.List;
-
@Service
public class TicketCommentService {
- private final TicketRepository ticketRepository;
- private final TicketCommentRepository ticketCommentRepository;
- private final TicketCommentMapper ticketCommentMapper;
-
- public TicketCommentService(TicketRepository ticketRepository,
- TicketCommentRepository ticketCommentRepository,
- TicketCommentMapper ticketCommentMapper) {
- this.ticketRepository = ticketRepository;
- this.ticketCommentRepository = ticketCommentRepository;
- this.ticketCommentMapper = ticketCommentMapper;
- }
-
- @Transactional
- public CommentViewDTO addComment(Long ticketId, CommentCreateDTO dto, User author, String token) {
- Ticket ticket = ticketRepository.findById(ticketId)
- .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found"));
-
- boolean internalNote = dto.isInternalNote();
-
- checkAccess(ticket, author, token);
- checkInternalNotePermission(internalNote, author);
-
- TicketComment comment = new TicketComment();
- comment.setTicket(ticket);
- comment.setAuthor(author);
- comment.setMessage(dto.getMessage());
- comment.setInternalNote(internalNote);
-
- TicketComment savedComment = ticketCommentRepository.save(comment);
- return ticketCommentMapper.entityToViewDTO(savedComment);
+ private final TicketRepository ticketRepository;
+ private final TicketCommentRepository ticketCommentRepository;
+ private final TicketCommentMapper ticketCommentMapper;
+
+ public TicketCommentService(
+ TicketRepository ticketRepository,
+ TicketCommentRepository ticketCommentRepository,
+ TicketCommentMapper ticketCommentMapper) {
+ this.ticketRepository = ticketRepository;
+ this.ticketCommentRepository = ticketCommentRepository;
+ this.ticketCommentMapper = ticketCommentMapper;
+ }
+
+ @Transactional
+ public CommentViewDTO addComment(Long ticketId, CommentCreateDTO dto, User author, String token) {
+ Ticket ticket =
+ ticketRepository
+ .findById(ticketId)
+ .orElseThrow(
+ () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found"));
+
+ boolean internalNote = dto.isInternalNote();
+
+ checkAccess(ticket, author, token);
+ checkInternalNotePermission(internalNote, author);
+
+ TicketComment comment = new TicketComment();
+ comment.setTicket(ticket);
+ comment.setAuthor(author);
+ comment.setMessage(dto.getMessage());
+ comment.setInternalNote(internalNote);
+
+ TicketComment savedComment = ticketCommentRepository.save(comment);
+ return ticketCommentMapper.entityToViewDTO(savedComment);
+ }
+
+ @Transactional(readOnly = true)
+ public List getComments(Long ticketId, User user, String token) {
+
+ Ticket ticket =
+ ticketRepository
+ .findById(ticketId)
+ .orElseThrow(
+ () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found"));
+
+ checkAccess(ticket, user, token);
+
+ boolean isReporter = user != null && user.getRole() == Role.REPORTER;
+ boolean isAnonymous = user == null;
+
+ List all =
+ (isReporter || isAnonymous)
+ ? ticketCommentRepository.findByTicketIdAndInternalNoteFalseOrderByCreatedAtAsc(
+ ticketId)
+ : ticketCommentRepository.findByTicketIdOrderByCreatedAtAsc(ticketId);
+
+ return all.stream().map(ticketCommentMapper::entityToViewDTO).toList();
+ }
+
+ // helpers
+ private void checkAccess(Ticket ticket, User user, String token) {
+ // Authenticated
+ if (user != null) {
+ if (user.getRole() == Role.ADMIN) return;
+
+ if (user.getRole() == Role.INVESTIGATOR
+ && ticket.getInvestigator() != null
+ && ticket.getInvestigator().getId().equals(user.getId())) {
+ return;
+ }
+
+ if (user.getRole() == Role.REPORTER
+ && ticket.getReporter() != null
+ && ticket.getReporter().getId().equals(user.getId())) {
+ return;
+ }
}
- @Transactional(readOnly = true)
- public List getComments(Long ticketId, User user, String token) {
-
- Ticket ticket = ticketRepository.findById(ticketId)
- .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found"));
-
- checkAccess(ticket, user, token);
-
- boolean isReporter = user != null && user.getRole() == Role.REPORTER;
- boolean isAnonymous = user == null;
-
- List all = (isReporter || isAnonymous)
- ? ticketCommentRepository.findByTicketIdAndInternalNoteFalseOrderByCreatedAtAsc(ticketId)
- : ticketCommentRepository.findByTicketIdOrderByCreatedAtAsc(ticketId);
-
- return all.stream()
- .map(ticketCommentMapper::entityToViewDTO)
- .toList();
+ // Anonymous
+ if (token != null && !token.isBlank() && token.equals(ticket.getReporterToken())) {
+ return;
}
- // helpers
- private void checkAccess(Ticket ticket, User user, String token) {
- // Authenticated
- if (user != null) {
- if (user.getRole() == Role.ADMIN) return;
-
- if (user.getRole() == Role.INVESTIGATOR &&
- ticket.getInvestigator() != null &&
- ticket.getInvestigator().getId().equals(user.getId())) {
- return;
- }
-
- if (user.getRole() == Role.REPORTER &&
- ticket.getReporter() != null &&
- ticket.getReporter().getId().equals(user.getId())) {
- return;
- }
- }
-
- // Anonymous
- if (token != null && !token.isBlank()
- && token.equals(ticket.getReporterToken())) {
- return;
- }
-
- throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Access denied");
- }
+ throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Access denied");
+ }
- private void checkInternalNotePermission(boolean internalNote, User author) {
- if (!internalNote) return;
+ private void checkInternalNotePermission(boolean internalNote, User author) {
+ if (!internalNote) return;
- if (author == null || (author.getRole() != Role.ADMIN && author.getRole() != Role.INVESTIGATOR)) {
- throw new ResponseStatusException(
- HttpStatus.FORBIDDEN, "Only investigators/admins can create internal notes");
- }
+ if (author == null
+ || (author.getRole() != Role.ADMIN && author.getRole() != Role.INVESTIGATOR)) {
+ throw new ResponseStatusException(
+ HttpStatus.FORBIDDEN, "Only investigators/admins can create internal notes");
}
-}
\ 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 fb474a7..848b8a6 100644
--- a/src/main/java/org/example/alfs/services/TicketService.java
+++ b/src/main/java/org/example/alfs/services/TicketService.java
@@ -1,5 +1,8 @@
package org.example.alfs.services;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
import org.example.alfs.dto.ticket.TicketCreateDTO;
import org.example.alfs.dto.ticket.TicketViewDTO;
import org.example.alfs.entities.Ticket;
@@ -16,377 +19,345 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
@Service
public class TicketService {
- private final TicketRepository ticketRepository;
- private final TicketMapper ticketMapper;
- private final SecurityUtils securityUtils;
- private final UserRepository userRepository;
- private final AuditService auditService;
-
- public TicketService(TicketRepository ticketRepository,
- TicketMapper ticketMapper,
- SecurityUtils securityUtils,
- UserRepository userRepository,
- AuditService auditService) {
- this.ticketRepository = ticketRepository;
- this.ticketMapper = ticketMapper;
- this.securityUtils = securityUtils;
- this.userRepository = userRepository;
- this.auditService = auditService;
+ private final TicketRepository ticketRepository;
+ private final TicketMapper ticketMapper;
+ private final SecurityUtils securityUtils;
+ private final UserRepository userRepository;
+ private final AuditService auditService;
+
+ public TicketService(
+ TicketRepository ticketRepository,
+ TicketMapper ticketMapper,
+ SecurityUtils securityUtils,
+ UserRepository userRepository,
+ AuditService auditService) {
+ this.ticketRepository = ticketRepository;
+ this.ticketMapper = ticketMapper;
+ this.securityUtils = securityUtils;
+ this.userRepository = userRepository;
+ this.auditService = auditService;
+ }
+
+ // createNewTicket
+ @Transactional
+ public TicketViewDTO createNewTicket(TicketCreateDTO dto) {
+
+ Ticket ticket = new Ticket();
+
+ ticket.setTitle(dto.getTitle());
+ ticket.setDescription(dto.getDescription());
+
+ User user = securityUtils.getCurrentUserOrNull();
+
+ String token = null;
+
+ if (user != null) {
+ ticket.setReporter(user);
+ } else {
+ token = java.util.UUID.randomUUID().toString();
+ ticket.setReporterToken(token);
}
- //createNewTicket
- @Transactional
- public TicketViewDTO createNewTicket(TicketCreateDTO dto) {
+ Ticket saved = ticketRepository.save(ticket);
- Ticket ticket = new Ticket();
+ auditService.log(AuditAction.CREATED, "title", null, saved.getTitle(), saved, user);
- ticket.setTitle(dto.getTitle());
- ticket.setDescription(dto.getDescription());
+ TicketViewDTO view = ticketMapper.entityToViewDTO(saved);
- User user = securityUtils.getCurrentUserOrNull();
+ if (token != null) {
+ view.setToken(token);
+ }
- String token = null;
+ return view;
+ }
- if (user != null) {
- ticket.setReporter(user);
- } else {
- token = java.util.UUID.randomUUID().toString();
- ticket.setReporterToken(token);
- }
+ // View by token
+ public TicketViewDTO getTicketByToken(String token) {
+ Ticket ticket =
+ ticketRepository
+ .findByReporterToken(token)
+ .orElseThrow(
+ () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found"));
- Ticket saved = ticketRepository.save(ticket);
+ TicketViewDTO view = ticketMapper.entityToViewDTO(ticket);
- auditService.log(
- AuditAction.CREATED,
- "title",
- null,
- saved.getTitle(),
- saved,
- user
- );
+ view.setToken(ticket.getReporterToken());
- TicketViewDTO view = ticketMapper.entityToViewDTO(saved);
+ return view;
+ }
- if (token != null) {
- view.setToken(token);
- }
+ // findById
+ public TicketViewDTO getTicketById(Long id) {
+ Ticket ticket =
+ ticketRepository
+ .findById(id)
+ .orElseThrow(
+ () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found"));
- return view;
- }
+ checkAccess(ticket);
+ return ticketMapper.entityToViewDTO(ticket);
+ }
- // View by token
- public TicketViewDTO getTicketByToken(String token) {
- Ticket ticket = ticketRepository.findByReporterToken(token)
- .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found"));
+ // Get all tickets for a reporter
+ public List getMyTickets() {
+ User user = requireCurrentUser();
- TicketViewDTO view = ticketMapper.entityToViewDTO(ticket);
+ return ticketRepository.findByReporterId(user.getId()).stream()
+ .map(ticketMapper::entityToViewDTO)
+ .toList();
+ }
- view.setToken(ticket.getReporterToken());
+ // Get all tickets assigned to me
+ public List getMyAssignedTickets() {
+ User user = requireCurrentUser();
- return view;
- }
+ return ticketRepository.findByInvestigatorId(user.getId()).stream()
+ .map(ticketMapper::entityToViewDTO)
+ .toList();
+ }
- //findById
- public TicketViewDTO getTicketById(Long id) {
- Ticket ticket = ticketRepository.findById(id)
- .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found"));
+ // this is for admin page
+ public List getAllTickets() {
- checkAccess(ticket);
+ User user = requireCurrentUser();
+ requireAdmin(user);
- return ticketMapper.entityToViewDTO(ticket);
- }
+ return ticketRepository.findAll().stream().map(ticketMapper::entityToViewDTO).toList();
+ }
- // Get all tickets for a reporter
- public List getMyTickets() {
- User user = requireCurrentUser();
+ // ----------------- filters -----------------
- return ticketRepository.findByReporterId(user.getId())
- .stream()
- .map(ticketMapper::entityToViewDTO)
- .toList();
- }
-
- // Get all tickets assigned to me
- public List getMyAssignedTickets() {
- User user = requireCurrentUser();
+ public List getTicketsByStatus(TicketStatus status) {
+ User user = requireCurrentUser();
+ requireAdmin(user);
- return ticketRepository.findByInvestigatorId(user.getId())
- .stream()
- .map(ticketMapper::entityToViewDTO)
- .toList();
- }
+ return ticketRepository.findByStatus(status).stream()
+ .map(ticketMapper::entityToViewDTO)
+ .toList();
+ }
- // this is for admin page
- public List getAllTickets() {
+ public List getTicketsByStatusAndInvestigator(
+ TicketStatus status, Long investigatorId) {
+ User user = requireCurrentUser();
- User user = requireCurrentUser();
- requireAdmin(user);
-
- return ticketRepository.findAll()
- .stream()
- .map(ticketMapper::entityToViewDTO)
- .toList();
+ if (user.getRole() == Role.ADMIN) {
+ return ticketRepository.findByStatusAndInvestigatorId(status, investigatorId).stream()
+ .map(ticketMapper::entityToViewDTO)
+ .toList();
}
- // ----------------- filters -----------------
-
- public List getTicketsByStatus(TicketStatus status) {
- User user = requireCurrentUser();
- requireAdmin(user);
+ if (user.getRole() == Role.INVESTIGATOR && user.getId().equals(investigatorId)) {
- return ticketRepository.findByStatus(status)
- .stream()
- .map(ticketMapper::entityToViewDTO)
- .toList();
+ return ticketRepository.findByStatusAndInvestigatorId(status, investigatorId).stream()
+ .map(ticketMapper::entityToViewDTO)
+ .toList();
}
- public List getTicketsByStatusAndInvestigator(TicketStatus status, Long investigatorId) {
- User user = requireCurrentUser();
+ throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Access denied");
+ }
- if (user.getRole() == Role.ADMIN) {
- return ticketRepository.findByStatusAndInvestigatorId(status, investigatorId)
- .stream()
- .map(ticketMapper::entityToViewDTO)
- .toList();
- }
+ // ----------------- helpers -----------------
- if (user.getRole() == Role.INVESTIGATOR &&
- user.getId().equals(investigatorId)) {
+ private void checkAccess(Ticket ticket) {
+ User user = requireCurrentUser();
- return ticketRepository.findByStatusAndInvestigatorId(status, investigatorId)
- .stream()
- .map(ticketMapper::entityToViewDTO)
- .toList();
- }
+ if (user.getRole() == Role.ADMIN) return;
- throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Access denied");
+ if (user.getRole() == Role.INVESTIGATOR) {
+ if (ticket.getInvestigator() != null
+ && ticket.getInvestigator().getId().equals(user.getId())) {
+ return;
+ }
}
- // ----------------- helpers -----------------
-
- private void checkAccess(Ticket ticket) {
- User user = requireCurrentUser();
-
- if (user.getRole() == Role.ADMIN) return;
-
- if (user.getRole() == Role.INVESTIGATOR) {
- if (ticket.getInvestigator() != null &&
- ticket.getInvestigator().getId().equals(user.getId())) {
- return;
- }
- }
-
- if (user.getRole() == Role.REPORTER) {
- if (ticket.getReporter() != null &&
- ticket.getReporter().getId().equals(user.getId())) {
- return;
- }
- }
-
- throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Access denied");
+ if (user.getRole() == Role.REPORTER) {
+ if (ticket.getReporter() != null && ticket.getReporter().getId().equals(user.getId())) {
+ return;
+ }
}
- private void requireAdmin(User user) {
- if (user.getRole() != Role.ADMIN) {
- throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Access denied");
- }
+ throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Access denied");
+ }
+
+ private void requireAdmin(User user) {
+ if (user.getRole() != Role.ADMIN) {
+ throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Access denied");
}
+ }
- private User requireCurrentUser() {
- try {
- return securityUtils.getCurrentUser();
- } catch (RuntimeException ex) {
+ private User requireCurrentUser() {
+ try {
+ return securityUtils.getCurrentUser();
+ } catch (RuntimeException ex) {
- String message = ex.getMessage();
+ String message = ex.getMessage();
- boolean authFailure =
- "No authenticated user in security context".equals(message) ||
- "Authenticated user not found in database".equals(message);
+ boolean authFailure =
+ "No authenticated user in security context".equals(message)
+ || "Authenticated user not found in database".equals(message);
- if (authFailure) {
- throw new ResponseStatusException(
- HttpStatus.UNAUTHORIZED,
- "Authentication required",
- ex
- );
- }
+ if (authFailure) {
+ throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Authentication required", ex);
+ }
- throw ex;
- }
+ throw ex;
+ }
+ }
+
+ // ----------------- status logic -----------------
+ @Transactional
+ public TicketViewDTO updateTicketStatus(Long id, TicketStatus newStatus) {
+
+ User user = requireCurrentUser();
+ Ticket ticket =
+ ticketRepository
+ .findById(id)
+ .orElseThrow(
+ () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found"));
+
+ if (user.getRole() != Role.ADMIN) {
+ boolean isAssignedInvestigator =
+ user.getRole() == Role.INVESTIGATOR
+ && ticket.getInvestigator() != null
+ && ticket.getInvestigator().getId().equals(user.getId());
+
+ if (!isAssignedInvestigator) {
+ throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Access denied");
+ }
}
+ TicketStatus currentStatus = ticket.getStatus();
- // ----------------- status logic -----------------
- @Transactional
- public TicketViewDTO updateTicketStatus(Long id, TicketStatus newStatus) {
-
- User user = requireCurrentUser();
- Ticket ticket = ticketRepository.findById(id)
- .orElseThrow(() -> new ResponseStatusException(
- HttpStatus.NOT_FOUND, "Ticket not found"));
-
- if (user.getRole() != Role.ADMIN) {
- boolean isAssignedInvestigator =
- user.getRole() == Role.INVESTIGATOR &&
- ticket.getInvestigator() != null &&
- ticket.getInvestigator().getId().equals(user.getId());
+ if (currentStatus == newStatus) {
+ return ticketMapper.entityToViewDTO(ticket);
+ }
- if (!isAssignedInvestigator) {
- throw new ResponseStatusException(
- HttpStatus.FORBIDDEN, "Access denied");
- }
- }
+ Set allowedTransitions =
+ ALLOWED_TRANSITIONS.getOrDefault(currentStatus, Set.of());
- TicketStatus currentStatus = ticket.getStatus();
+ boolean isValidTransition = allowedTransitions.contains(newStatus);
- if (currentStatus == newStatus) {
- return ticketMapper.entityToViewDTO(ticket);
- }
+ if (!isValidTransition) {
+ throw new ResponseStatusException(
+ HttpStatus.BAD_REQUEST, "Invalid transition from " + currentStatus + " to " + newStatus);
+ }
- Set allowedTransitions = ALLOWED_TRANSITIONS.getOrDefault(currentStatus, Set.of());
+ if (newStatus == TicketStatus.IN_PROGRESS && ticket.getInvestigator() == null) {
+ throw new ResponseStatusException(
+ HttpStatus.BAD_REQUEST, "Cannot move to IN_PROGRESS without investigator");
+ }
- boolean isValidTransition = allowedTransitions.contains(newStatus);
+ ticket.setStatus(newStatus);
- if (!isValidTransition) {
- throw new ResponseStatusException(
- HttpStatus.BAD_REQUEST, "Invalid transition from " + currentStatus + " to " + newStatus);
- }
+ Ticket savedTicket = ticketRepository.save(ticket);
- if (newStatus == TicketStatus.IN_PROGRESS && ticket.getInvestigator() == null) {
- throw new ResponseStatusException(
- HttpStatus.BAD_REQUEST, "Cannot move to IN_PROGRESS without investigator");
- }
+ auditService.log(
+ AuditAction.STATUS_CHANGED,
+ "status",
+ currentStatus.name(),
+ newStatus.name(),
+ savedTicket,
+ user);
+ return ticketMapper.entityToViewDTO(savedTicket);
+ }
- ticket.setStatus(newStatus);
+ private static final Map> ALLOWED_TRANSITIONS =
+ Map.of(
+ TicketStatus.OPEN, Set.of(TicketStatus.IN_PROGRESS),
+ TicketStatus.IN_PROGRESS, Set.of(TicketStatus.RESOLVED),
+ TicketStatus.RESOLVED, Set.of(TicketStatus.CLOSED),
+ TicketStatus.CLOSED, Set.of());
- Ticket savedTicket = ticketRepository.save(ticket);
+ @Transactional
+ public TicketViewDTO assignInvestigator(Long id, Long investigatorId) {
- auditService.log(
- AuditAction.STATUS_CHANGED,
- "status",
- currentStatus.name(),
- newStatus.name(),
- savedTicket,
- user
- );
- return ticketMapper.entityToViewDTO(savedTicket);
+ if (investigatorId == null) {
+ throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Investigator ID is required");
}
- private static final Map> ALLOWED_TRANSITIONS = Map.of(
- TicketStatus.OPEN, Set.of(TicketStatus.IN_PROGRESS),
- TicketStatus.IN_PROGRESS, Set.of(TicketStatus.RESOLVED),
- TicketStatus.RESOLVED, Set.of(TicketStatus.CLOSED),
- TicketStatus.CLOSED, Set.of()
- );
-
+ User user = requireCurrentUser();
+ requireAdmin(user);
- @Transactional
- public TicketViewDTO assignInvestigator(Long id, Long investigatorId) {
+ Ticket ticket =
+ ticketRepository
+ .findById(id)
+ .orElseThrow(
+ () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found"));
- if (investigatorId == null) {
- throw new ResponseStatusException(
- HttpStatus.BAD_REQUEST, "Investigator ID is required");
- }
-
- User user = requireCurrentUser();
- requireAdmin(user);
+ if (ticket.getInvestigator() != null) {
+ throw new ResponseStatusException(
+ HttpStatus.CONFLICT, "Ticket already has an investigator assigned");
+ }
- Ticket ticket = ticketRepository.findById(id)
- .orElseThrow(() -> new ResponseStatusException(
- HttpStatus.NOT_FOUND, "Ticket not found"));
+ if (ticket.getStatus() != TicketStatus.OPEN) {
+ throw new ResponseStatusException(
+ HttpStatus.BAD_REQUEST, "Ticket must be in OPEN status to assign an investigator");
+ }
- if (ticket.getInvestigator() != null) {
- throw new ResponseStatusException(
- HttpStatus.CONFLICT, "Ticket already has an investigator assigned");
- }
+ User investigator =
+ userRepository
+ .findById(investigatorId)
+ .orElseThrow(
+ () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Investigator not found"));
- if (ticket.getStatus() != TicketStatus.OPEN) {
- throw new ResponseStatusException(
- HttpStatus.BAD_REQUEST, "Ticket must be in OPEN status to assign an investigator");
- }
+ if (investigator.getRole() != Role.INVESTIGATOR) {
+ throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "User is not an investigator");
+ }
- User investigator = userRepository.findById(investigatorId)
- .orElseThrow(() -> new ResponseStatusException(
- HttpStatus.NOT_FOUND, "Investigator not found"));
+ TicketStatus oldStatus = ticket.getStatus();
+ ticket.setInvestigator(investigator);
+ ticket.setStatus(TicketStatus.IN_PROGRESS);
- if (investigator.getRole() != Role.INVESTIGATOR) {
- throw new ResponseStatusException(
- HttpStatus.BAD_REQUEST, "User is not an investigator");
- }
+ Ticket savedTicket = ticketRepository.save(ticket);
- TicketStatus oldStatus = ticket.getStatus();
- ticket.setInvestigator(investigator);
- ticket.setStatus(TicketStatus.IN_PROGRESS);
+ auditService.log(
+ AuditAction.STATUS_CHANGED,
+ "status",
+ oldStatus.name(),
+ TicketStatus.IN_PROGRESS.name(),
+ savedTicket,
+ user);
- Ticket savedTicket = ticketRepository.save(ticket);
+ auditService.log(
+ AuditAction.ASSIGNED, "investigator", null, investigator.getUsername(), savedTicket, user);
+ return ticketMapper.entityToViewDTO(savedTicket);
+ }
- auditService.log(
- AuditAction.STATUS_CHANGED,
- "status",
- oldStatus.name(),
- TicketStatus.IN_PROGRESS.name(),
- savedTicket,
- user
- );
+ @Transactional
+ public TicketViewDTO unassignInvestigator(Long id) {
- auditService.log(
- AuditAction.ASSIGNED,
- "investigator",
- null,
- investigator.getUsername(),
- savedTicket,
- user
- );
+ User user = requireCurrentUser();
+ requireAdmin(user);
+ Ticket ticket =
+ ticketRepository
+ .findById(id)
+ .orElseThrow(
+ () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found"));
- return ticketMapper.entityToViewDTO(savedTicket);
+ if (ticket.getInvestigator() == null) {
+ throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "No investigator assigned");
}
- @Transactional
- public TicketViewDTO unassignInvestigator(Long id) {
-
- User user = requireCurrentUser();
- requireAdmin(user);
-
- Ticket ticket = ticketRepository.findById(id)
- .orElseThrow(() -> new ResponseStatusException(
- HttpStatus.NOT_FOUND, "Ticket not found"));
-
- if (ticket.getInvestigator() == null) {
- throw new ResponseStatusException(
- HttpStatus.BAD_REQUEST, "No investigator assigned");
- }
-
- if (ticket.getStatus() != TicketStatus.IN_PROGRESS) {
- throw new ResponseStatusException(
- HttpStatus.BAD_REQUEST, "Must be IN_PROGRESS");
- }
+ if (ticket.getStatus() != TicketStatus.IN_PROGRESS) {
+ throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Must be IN_PROGRESS");
+ }
- String oldInvestigator = ticket.getInvestigator().getUsername();
+ String oldInvestigator = ticket.getInvestigator().getUsername();
- ticket.setInvestigator(null);
- ticket.setStatus(TicketStatus.OPEN);
+ ticket.setInvestigator(null);
+ ticket.setStatus(TicketStatus.OPEN);
- Ticket savedTicket = ticketRepository.save(ticket);
+ Ticket savedTicket = ticketRepository.save(ticket);
- auditService.log(
- AuditAction.UNASSIGNED,
- "investigator",
- oldInvestigator,
- null,
- savedTicket,
- user
- );
+ auditService.log(
+ AuditAction.UNASSIGNED, "investigator", oldInvestigator, null, savedTicket, user);
- return ticketMapper.entityToViewDTO(savedTicket);
- }
-}
\ No newline at end of file
+ return ticketMapper.entityToViewDTO(savedTicket);
+ }
+}
diff --git a/src/main/java/org/example/alfs/services/UserService.java b/src/main/java/org/example/alfs/services/UserService.java
index e7f5000..ea9e86a 100644
--- a/src/main/java/org/example/alfs/services/UserService.java
+++ b/src/main/java/org/example/alfs/services/UserService.java
@@ -1,22 +1,21 @@
package org.example.alfs.services;
+import java.util.List;
import org.example.alfs.entities.User;
import org.example.alfs.enums.Role;
import org.example.alfs.repositories.UserRepository;
import org.springframework.stereotype.Service;
-import java.util.List;
-
@Service
public class UserService {
- private final UserRepository userRepository;
+ private final UserRepository userRepository;
- public UserService(UserRepository userRepository) {
- this.userRepository = userRepository;
- }
+ public UserService(UserRepository userRepository) {
+ this.userRepository = userRepository;
+ }
- public List getAllInvestigators() {
- return userRepository.findByRole(Role.INVESTIGATOR);
- }
+ public List getAllInvestigators() {
+ return userRepository.findByRole(Role.INVESTIGATOR);
+ }
}
diff --git a/src/main/java/org/example/alfs/services/storage/MinioStorageService.java b/src/main/java/org/example/alfs/services/storage/MinioStorageService.java
index a051f81..c54b6e1 100644
--- a/src/main/java/org/example/alfs/services/storage/MinioStorageService.java
+++ b/src/main/java/org/example/alfs/services/storage/MinioStorageService.java
@@ -5,67 +5,58 @@
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
+import java.io.InputStream;
+import java.util.UUID;
import org.example.alfs.config.S3Properties;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
-import java.io.InputStream;
-import java.util.UUID;
-
@Service
public class MinioStorageService {
- private final MinioClient minioClient;
- private final S3Properties props;
-
- public MinioStorageService(MinioClient minioClient, S3Properties props) {
- this.minioClient = minioClient;
- this.props = props;
- }
+ private final MinioClient minioClient;
+ private final S3Properties props;
- /**
- * Laddar upp en fil till S3/MinIO och returnerar dess objectKey.
- */
- public String upload(MultipartFile file) throws Exception {
- String fileName = file.getOriginalFilename();
- if (fileName == null || fileName.isBlank()) {
- fileName = "file";
- }
- String objectKey = UUID.randomUUID() + "/" + sanitize(fileName);
+ public MinioStorageService(MinioClient minioClient, S3Properties props) {
+ this.minioClient = minioClient;
+ this.props = props;
+ }
- try (InputStream is = file.getInputStream()) {
- String contentType = file.getContentType() != null ? file.getContentType() : "application/octet-stream";
- PutObjectArgs args = PutObjectArgs.builder()
- .bucket(props.getBucket())
- .object(objectKey)
- .contentType(contentType)
- .stream(is, file.getSize(), -1)
- .build();
- minioClient.putObject(args);
- }
-
- return objectKey;
+ /** Laddar upp en fil till S3/MinIO och returnerar dess objectKey. */
+ public String upload(MultipartFile file) throws Exception {
+ String fileName = file.getOriginalFilename();
+ if (fileName == null || fileName.isBlank()) {
+ fileName = "file";
}
-
- public GetObjectResponse download(String objectKey) throws Exception {
- return minioClient.getObject(
- GetObjectArgs.builder()
- .bucket(props.getBucket())
- .object(objectKey)
- .build()
- );
+ String objectKey = UUID.randomUUID() + "/" + sanitize(fileName);
+
+ try (InputStream is = file.getInputStream()) {
+ String contentType =
+ file.getContentType() != null ? file.getContentType() : "application/octet-stream";
+ PutObjectArgs args =
+ PutObjectArgs.builder()
+ .bucket(props.getBucket())
+ .object(objectKey)
+ .contentType(contentType)
+ .stream(is, file.getSize(), -1)
+ .build();
+ minioClient.putObject(args);
}
- public void delete(String objectKey) throws Exception {
- minioClient.removeObject(
- RemoveObjectArgs.builder()
- .bucket(props.getBucket())
- .object(objectKey)
- .build()
- );
- }
+ return objectKey;
+ }
- private String sanitize(String name) {
- return name.replace("\\", "_").replace("/", "_");
- }
+ public GetObjectResponse download(String objectKey) throws Exception {
+ return minioClient.getObject(
+ GetObjectArgs.builder().bucket(props.getBucket()).object(objectKey).build());
+ }
+
+ public void delete(String objectKey) throws Exception {
+ minioClient.removeObject(
+ RemoveObjectArgs.builder().bucket(props.getBucket()).object(objectKey).build());
+ }
+
+ private String sanitize(String name) {
+ return name.replace("\\", "_").replace("/", "_");
+ }
}
diff --git a/src/test/java/org/example/alfs/AlfsApplicationTests.java b/src/test/java/org/example/alfs/AlfsApplicationTests.java
index 58c6a03..4751ae0 100644
--- a/src/test/java/org/example/alfs/AlfsApplicationTests.java
+++ b/src/test/java/org/example/alfs/AlfsApplicationTests.java
@@ -6,8 +6,6 @@
@SpringBootTest
class AlfsApplicationTests {
- @Test
- void contextLoads() {
- }
-
+ @Test
+ void contextLoads() {}
}
diff --git a/src/test/java/org/example/alfs/ArchitectureTest.java b/src/test/java/org/example/alfs/ArchitectureTest.java
index fc4d8af..1d94420 100644
--- a/src/test/java/org/example/alfs/ArchitectureTest.java
+++ b/src/test/java/org/example/alfs/ArchitectureTest.java
@@ -1,116 +1,126 @@
package org.example.alfs;
+import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
+
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
+import com.tngtech.archunit.core.importer.ImportOption;
import org.junit.jupiter.api.Test;
-import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
-
-
/**
* Architecture Test
*
- * Verifies that the project follows a clean layered architecture
- * and enforces separation of concerns using ArchUnit.
+ * Verifies that the project follows a clean layered architecture and enforces separation of
+ * concerns using ArchUnit.
*/
-
class ArchitectureTest {
- private final JavaClasses classes =
- new ClassFileImporter().importPackages("org.example.alfs");
-
- /**
- * Layer Rules
- *
- * Enforces a clean layered architecture:
- * Controller → Service → Repository.
- * Controllers must not access repositories directly,
- * and services must not depend on controllers.
- */
-
- @Test
- void controllers_should_only_access_services_not_repositories() {
- noClasses()
- .that().resideInAPackage("..controllers..")
- .should().dependOnClassesThat()
- .resideInAPackage("..repositories..")
- .check(classes);
- }
-
- @Test
- void services_should_not_depend_on_controllers() {
- noClasses()
- .that().resideInAPackage("..services..")
- .should().dependOnClassesThat()
- .resideInAPackage("..controllers..")
- .check(classes);
- }
-
- /**
- * Repository Access Rule
- *
- * Controllers, DTOs and mappers must not access repositories directly.
- * Repository access should go through the service layer.
- */
- @Test
- void controllers_dto_and_mapper_should_not_access_repositories() {
- noClasses()
- .that().resideInAnyPackage("..controllers..", "..dto..", "..mapper..")
- .should().dependOnClassesThat()
- .resideInAPackage("..repositories..")
- .check(classes);
- }
-
- /**
- * Package Rules
- *
- * Classes should be in correct packages.
- */
-
- @Test
- void controllers_should_be_in_controller_package() {
- classes()
- .that().haveSimpleNameEndingWith("Controller")
- .should().resideInAPackage("..controllers..")
- .check(classes);
- }
-
- /**
- * Service Package Rule
- *
- * Services should reside in the services package.
- * An exception is made for JwtService, which is located in the security package.
- */
- @Test
- void services_should_be_in_services_package() {
- classes()
- .that().haveSimpleNameEndingWith("Service")
- .should().resideInAnyPackage("..services..", "..security..")
- .check(classes);
- }
-
- @Test
- void repositories_should_be_in_repositories_package() {
- classes()
- .that().haveSimpleNameEndingWith("Repository")
- .should().resideInAPackage("..repositories..")
- .check(classes);
- }
-
- /**
- * Dtos should not depend on entities
- *
- * Entities represent database structure and may change due to internal requirements,
- * while DTOs define what is exposed externally. If DTOs depend on entities,
- * changes in the database can unintentionally affect the API.
- */
-
- @Test
- void dto_should_not_depend_on_entities() {
- noClasses()
- .that().resideInAPackage("..dto..")
- .should().dependOnClassesThat()
- .resideInAPackage("..entities..")
- .check(classes);
- }
-}
\ No newline at end of file
+ private final JavaClasses classes =
+ new ClassFileImporter()
+ .withImportOption(new ImportOption.DoNotIncludeTests())
+ .importPackages("org.example.alfs");
+
+ /**
+ * Layer Rules
+ *
+ *
Enforces a clean layered architecture: Controller → Service → Repository. Controllers must
+ * not access repositories directly, and services must not depend on controllers.
+ */
+ @Test
+ void controllers_should_only_access_services_not_repositories() {
+ noClasses()
+ .that()
+ .resideInAPackage("..controllers..")
+ .should()
+ .dependOnClassesThat()
+ .resideInAPackage("..repositories..")
+ .check(classes);
+ }
+
+ @Test
+ void services_should_not_depend_on_controllers() {
+ noClasses()
+ .that()
+ .resideInAPackage("..services..")
+ .should()
+ .dependOnClassesThat()
+ .resideInAPackage("..controllers..")
+ .check(classes);
+ }
+
+ /**
+ * Repository Access Rule
+ *
+ *
Controllers, DTOs and mappers must not access repositories directly. Repository access
+ * should go through the service layer.
+ */
+ @Test
+ void controllers_dto_and_mapper_should_not_access_repositories() {
+ noClasses()
+ .that()
+ .resideInAnyPackage("..controllers..", "..dto..", "..mapper..")
+ .should()
+ .dependOnClassesThat()
+ .resideInAPackage("..repositories..")
+ .check(classes);
+ }
+
+ /**
+ * Package Rules
+ *
+ *
Classes should be in correct packages.
+ */
+ @Test
+ void controllers_should_be_in_controller_package() {
+ classes()
+ .that()
+ .haveSimpleNameEndingWith("Controller")
+ .should()
+ .resideInAPackage("..controllers..")
+ .check(classes);
+ }
+
+ /**
+ * Service Package Rule
+ *
+ *
Services should reside in the services package. An exception is made for JwtService, which
+ * is located in the security package.
+ */
+ @Test
+ void services_should_be_in_services_package() {
+ classes()
+ .that()
+ .haveSimpleNameEndingWith("Service")
+ .should()
+ .resideInAnyPackage("..services..", "..security..")
+ .check(classes);
+ }
+
+ @Test
+ void repositories_should_be_in_repositories_package() {
+ classes()
+ .that()
+ .haveSimpleNameEndingWith("Repository")
+ .should()
+ .resideInAPackage("..repositories..")
+ .check(classes);
+ }
+
+ /**
+ * Dtos should not depend on entities
+ *
+ *
Entities represent database structure and may change due to internal requirements, while
+ * DTOs define what is exposed externally. If DTOs depend on entities, changes in the database can
+ * unintentionally affect the API.
+ */
+ @Test
+ void dto_should_not_depend_on_entities() {
+ noClasses()
+ .that()
+ .resideInAPackage("..dto..")
+ .should()
+ .dependOnClassesThat()
+ .resideInAPackage("..entities..")
+ .check(classes);
+ }
+}
diff --git a/src/test/java/org/example/alfs/integration/TicketControllerIT.java b/src/test/java/org/example/alfs/integration/TicketControllerIT.java
index 683195f..9e4c282 100644
--- a/src/test/java/org/example/alfs/integration/TicketControllerIT.java
+++ b/src/test/java/org/example/alfs/integration/TicketControllerIT.java
@@ -1,5 +1,10 @@
package org.example.alfs.integration;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
import org.example.alfs.dto.ticket.TicketCreateDTO;
import org.example.alfs.entities.User;
import org.example.alfs.enums.Role;
@@ -17,197 +22,199 @@
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
-
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
@DisplayName("TicketController Integration Tests")
class TicketControllerIT {
- @Autowired
- private MockMvc mockMvc;
-
- @Autowired
- private TicketService ticketService;
- @Autowired
- private UserRepository userRepository;
- @Autowired
- private TicketRepository ticketRepository;
-
- private Long ticketId;
- private User admin;
- private User investigator;
- private User reporter;
-
- @BeforeEach
- void setUp() {
- admin = new User();
- admin.setRole(Role.ADMIN);
- admin.setUsername("admin");
- admin.setPasswordHash("hashed-password");
- admin = userRepository.save(admin);
-
- investigator = new User();
- investigator.setRole(Role.INVESTIGATOR);
- investigator.setUsername("investigator");
- investigator.setPasswordHash("hashed-password");
- investigator = userRepository.save(investigator);
-
- reporter = new User();
- reporter.setRole(Role.REPORTER);
- reporter.setUsername("reporter");
- reporter.setPasswordHash("hashed-password");
- reporter = userRepository.save(reporter);
-
- TicketCreateDTO dto = new TicketCreateDTO();
- dto.setTitle("Test");
- dto.setDescription("Test");
- ticketId = ticketService.createNewTicket(dto).getId();
+ @Autowired private MockMvc mockMvc;
+
+ @Autowired private TicketService ticketService;
+ @Autowired private UserRepository userRepository;
+ @Autowired private TicketRepository ticketRepository;
+
+ private Long ticketId;
+ private User admin;
+ private User investigator;
+ private User reporter;
+
+ @BeforeEach
+ void setUp() {
+ admin = new User();
+ admin.setRole(Role.ADMIN);
+ admin.setUsername("admin");
+ admin.setPasswordHash("hashed-password");
+ admin = userRepository.save(admin);
+
+ investigator = new User();
+ investigator.setRole(Role.INVESTIGATOR);
+ investigator.setUsername("investigator");
+ investigator.setPasswordHash("hashed-password");
+ investigator = userRepository.save(investigator);
+
+ reporter = new User();
+ reporter.setRole(Role.REPORTER);
+ reporter.setUsername("reporter");
+ reporter.setPasswordHash("hashed-password");
+ reporter = userRepository.save(reporter);
+
+ TicketCreateDTO dto = new TicketCreateDTO();
+ dto.setTitle("Test");
+ dto.setDescription("Test");
+ ticketId = ticketService.createNewTicket(dto).getId();
+ }
+
+ @Nested
+ @DisplayName("Anonymous Reporter")
+ class AnonymousReporter {
+
+ @Test
+ @DisplayName("Anonymous reporter can access create form")
+ void anonymousReporter_canAccessCreateForm() throws Exception {
+ mockMvc
+ .perform(get("/tickets/create"))
+ .andExpect(status().isOk())
+ .andExpect(view().name("create"))
+ .andExpect(model().attributeExists("ticket"));
}
- @Nested
- @DisplayName("Anonymous Reporter")
- class AnonymousReporter {
-
- @Test
- @DisplayName("Anonymous reporter can access create form")
- void anonymousReporter_canAccessCreateForm() throws Exception {
- mockMvc.perform(get("/tickets/create"))
- .andExpect(status().isOk())
- .andExpect(view().name("create"))
- .andExpect(model().attributeExists("ticket"));
- }
-
- @Test
- @DisplayName("Anonymous reporter can view a created ticket with valid token")
- void anonymousReporter_validToken_returnsView() throws Exception {
- var ticket = ticketRepository.findById(ticketId).orElseThrow();
-
- mockMvc.perform(get("/tickets/token/" + ticket.getReporterToken()))
- .andExpect(status().isOk())
- .andExpect(view().name("view"))
- .andExpect(model().attributeExists("ticket"))
- .andExpect(model().attributeExists("comments"))
- .andExpect(model().attributeExists("attachments"))
- .andExpect(model().attributeExists("auditLogs"))
- .andExpect(model().attributeExists("investigators"))
- .andExpect(model().attributeExists("accessToken"));
- }
-
- @Test
- @DisplayName("Anonymous reporter is redirected when token is invalid")
- void anonymousReporter_invalidToken_redirectsToLogin() throws Exception {
- mockMvc.perform(get("/tickets/token/invalid-token"))
- .andExpect(status().is3xxRedirection())
- .andExpect(redirectedUrl("/login?tokenError=true"));
- }
-
- @Test
- @DisplayName("Anonymous reporter submits valid ticket and is redirected to ticket-created")
- void anonymousReporter_validPost_redirectsToTicketCreated() throws Exception {
- mockMvc.perform(post("/tickets/create")
- .param("title", "Test title")
- .param("description", "Test description"))
- .andExpect(status().is3xxRedirection())
- .andExpect(redirectedUrlPattern("/tickets/ticket-created?token=*"));
- }
-
- @Test
- @DisplayName("Anonymous reporter submits blank form and sees create page again")
- void anonymousReporter_blankPost_returnsCreateForm() throws Exception {
- mockMvc.perform(post("/tickets/create")
- .param("title", "")
- .param("description", ""))
- .andExpect(status().isOk())
- .andExpect(view().name("create"))
- .andExpect(model().attributeHasFieldErrors("ticket", "title", "description"));
- }
+ @Test
+ @DisplayName("Anonymous reporter can view a created ticket with valid token")
+ void anonymousReporter_validToken_returnsView() throws Exception {
+ var ticket = ticketRepository.findById(ticketId).orElseThrow();
+
+ mockMvc
+ .perform(get("/tickets/token/" + ticket.getReporterToken()))
+ .andExpect(status().isOk())
+ .andExpect(view().name("view"))
+ .andExpect(model().attributeExists("ticket"))
+ .andExpect(model().attributeExists("comments"))
+ .andExpect(model().attributeExists("attachments"))
+ .andExpect(model().attributeExists("auditLogs"))
+ .andExpect(model().attributeExists("investigators"))
+ .andExpect(model().attributeExists("accessToken"));
}
- @Nested
- @DisplayName("Authenticated reporter")
- class AuthenticatedReporter {
-
- @Test
- @WithMockUser(username = "reporter", roles = "REPORTER")
- @DisplayName("Reporter can view their own tickets")
- void reporter_canViewOwnTickets() throws Exception {
- mockMvc.perform(get("/tickets/my"))
- .andExpect(status().isOk())
- .andExpect(view().name("my-tickets"))
- .andExpect(model().attributeExists("tickets"));
- }
+ @Test
+ @DisplayName("Anonymous reporter is redirected when token is invalid")
+ void anonymousReporter_invalidToken_redirectsToLogin() throws Exception {
+ mockMvc
+ .perform(get("/tickets/token/invalid-token"))
+ .andExpect(status().is3xxRedirection())
+ .andExpect(redirectedUrl("/login?tokenError=true"));
}
- @Nested
- @DisplayName("Authenticated investigator")
- class AuthenticatedInvestigator {
-
- @Test
- @WithMockUser(username = "investigator", roles = "INVESTIGATOR")
- @DisplayName("Investigator can view their assigned tickets")
- void investigator_canViewAssignedTickets() throws Exception {
- mockMvc.perform(get("/tickets/assigned"))
- .andExpect(status().isOk())
- .andExpect(view().name("assigned-tickets"))
- .andExpect(model().attributeExists("tickets"));
- }
+ @Test
+ @DisplayName("Anonymous reporter submits valid ticket and is redirected to ticket-created")
+ void anonymousReporter_validPost_redirectsToTicketCreated() throws Exception {
+ mockMvc
+ .perform(
+ post("/tickets/create")
+ .param("title", "Test title")
+ .param("description", "Test description"))
+ .andExpect(status().is3xxRedirection())
+ .andExpect(redirectedUrlPattern("/tickets/ticket-created?token=*"));
+ }
+
+ @Test
+ @DisplayName("Anonymous reporter submits blank form and sees create page again")
+ void anonymousReporter_blankPost_returnsCreateForm() throws Exception {
+ mockMvc
+ .perform(post("/tickets/create").param("title", "").param("description", ""))
+ .andExpect(status().isOk())
+ .andExpect(view().name("create"))
+ .andExpect(model().attributeHasFieldErrors("ticket", "title", "description"));
+ }
+ }
+
+ @Nested
+ @DisplayName("Authenticated reporter")
+ class AuthenticatedReporter {
+
+ @Test
+ @WithMockUser(username = "reporter", roles = "REPORTER")
+ @DisplayName("Reporter can view their own tickets")
+ void reporter_canViewOwnTickets() throws Exception {
+ mockMvc
+ .perform(get("/tickets/my"))
+ .andExpect(status().isOk())
+ .andExpect(view().name("my-tickets"))
+ .andExpect(model().attributeExists("tickets"));
+ }
+ }
+
+ @Nested
+ @DisplayName("Authenticated investigator")
+ class AuthenticatedInvestigator {
+
+ @Test
+ @WithMockUser(username = "investigator", roles = "INVESTIGATOR")
+ @DisplayName("Investigator can view their assigned tickets")
+ void investigator_canViewAssignedTickets() throws Exception {
+ mockMvc
+ .perform(get("/tickets/assigned"))
+ .andExpect(status().isOk())
+ .andExpect(view().name("assigned-tickets"))
+ .andExpect(model().attributeExists("tickets"));
+ }
+ }
+
+ @Nested
+ @DisplayName("Admin")
+ class Admin {
+
+ @Test
+ @WithMockUser(username = "admin", roles = "ADMIN")
+ @DisplayName("Admin can view ticket by id")
+ void admin_canViewTicketById() throws Exception {
+ mockMvc
+ .perform(get("/tickets/{id}", ticketId))
+ .andExpect(status().isOk())
+ .andExpect(view().name("view"))
+ .andExpect(model().attributeExists("ticket"))
+ .andExpect(model().attributeExists("comments"))
+ .andExpect(model().attributeExists("attachments"))
+ .andExpect(model().attributeExists("auditLogs"))
+ .andExpect(model().attributeExists("investigators"));
+ }
+
+ @Test
+ @WithMockUser(username = "admin", roles = "ADMIN")
+ @DisplayName("Admin can assign an investigator to a ticket")
+ void admin_canAssignInvestigator() throws Exception {
+ mockMvc
+ .perform(
+ post("/tickets/{id}/assign", ticketId)
+ .param("investigatorId", investigator.getId().toString()))
+ .andExpect(status().is3xxRedirection())
+ .andExpect(redirectedUrl("/tickets/" + ticketId));
+ }
+
+ @Test
+ @WithMockUser(username = "admin", roles = "ADMIN")
+ @DisplayName("Admin can update status on a ticket")
+ void admin_canUpdateTicketStatus() throws Exception {
+ ticketService.assignInvestigator(ticketId, investigator.getId());
+
+ mockMvc
+ .perform(post("/tickets/{id}/status", ticketId).param("status", "RESOLVED"))
+ .andExpect(status().is3xxRedirection())
+ .andExpect(redirectedUrl("/tickets/" + ticketId));
}
- @Nested
- @DisplayName("Admin")
- class Admin {
-
- @Test
- @WithMockUser(username = "admin", roles = "ADMIN")
- @DisplayName("Admin can view ticket by id")
- void admin_canViewTicketById() throws Exception {
- mockMvc.perform(get("/tickets/{id}", ticketId))
- .andExpect(status().isOk())
- .andExpect(view().name("view"))
- .andExpect(model().attributeExists("ticket"))
- .andExpect(model().attributeExists("comments"))
- .andExpect(model().attributeExists("attachments"))
- .andExpect(model().attributeExists("auditLogs"))
- .andExpect(model().attributeExists("investigators"));
- }
-
- @Test
- @WithMockUser(username = "admin", roles = "ADMIN")
- @DisplayName("Admin can assign an investigator to a ticket")
- void admin_canAssignInvestigator() throws Exception {
- mockMvc.perform(post("/tickets/{id}/assign", ticketId)
- .param("investigatorId", investigator.getId().toString()))
- .andExpect(status().is3xxRedirection())
- .andExpect(redirectedUrl("/tickets/" + ticketId));
- }
-
- @Test
- @WithMockUser(username = "admin", roles = "ADMIN")
- @DisplayName("Admin can update status on a ticket")
- void admin_canUpdateTicketStatus() throws Exception {
- ticketService.assignInvestigator(ticketId, investigator.getId());
-
- mockMvc.perform(post("/tickets/{id}/status", ticketId)
- .param("status", "RESOLVED"))
- .andExpect(status().is3xxRedirection())
- .andExpect(redirectedUrl("/tickets/" + ticketId));
- }
-
- @Test
- @WithMockUser(username = "admin", roles = "ADMIN")
- @DisplayName("Admin cannot perform invalid status transition")
- void admin_cannotPerformInvalidStatusTransition() throws Exception {
- var ticket = ticketRepository.findById(ticketId).orElseThrow();
- assertEquals(org.example.alfs.enums.TicketStatus.OPEN, ticket.getStatus());
-
- mockMvc.perform(post("/tickets/{id}/status", ticketId)
- .param("status", "RESOLVED")) // Transition OPEN -> RESOLVED is invalid
- .andExpect(status().isBadRequest());
- }
+ @Test
+ @WithMockUser(username = "admin", roles = "ADMIN")
+ @DisplayName("Admin cannot perform invalid status transition")
+ void admin_cannotPerformInvalidStatusTransition() throws Exception {
+ var ticket = ticketRepository.findById(ticketId).orElseThrow();
+ assertEquals(org.example.alfs.enums.TicketStatus.OPEN, ticket.getStatus());
+
+ mockMvc
+ .perform(
+ post("/tickets/{id}/status", ticketId)
+ .param("status", "RESOLVED")) // Transition OPEN -> RESOLVED is invalid
+ .andExpect(status().isBadRequest());
}
-}
\ No newline at end of file
+ }
+}
diff --git a/src/test/java/org/example/alfs/services/AttachmentServiceTest.java b/src/test/java/org/example/alfs/services/AttachmentServiceTest.java
index d6bbb0c..2b74e87 100644
--- a/src/test/java/org/example/alfs/services/AttachmentServiceTest.java
+++ b/src/test/java/org/example/alfs/services/AttachmentServiceTest.java
@@ -1,5 +1,10 @@
package org.example.alfs.services;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Optional;
import org.example.alfs.entities.Attachment;
import org.example.alfs.entities.Ticket;
import org.example.alfs.entities.User;
@@ -20,238 +25,234 @@
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
-import java.util.Optional;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.Mockito.*;
-
@DisplayName("AttachmentService Test")
@ExtendWith(MockitoExtension.class)
class AttachmentServiceTest {
- @Mock
- private MinioStorageService storageService;
- @Mock
- private AttachmentRepository attachmentRepository;
- @Mock
- private TicketRepository ticketRepository;
- @Mock
- private AuditService auditService;
- @Mock
- private SecurityUtils securityUtils;
-
- @InjectMocks
- private AttachmentService attachmentService;
-
- private Ticket ticket;
- private User admin;
- private User investigator;
- private User reporter;
- private MultipartFile file;
-
- @BeforeEach
- void setUp() {
- admin = new User();
- admin.setId(100L);
- admin.setRole(Role.ADMIN);
-
- investigator = new User();
- investigator.setId(200L);
- investigator.setRole(Role.INVESTIGATOR);
-
- reporter = new User();
- reporter.setId(300L);
- reporter.setRole(Role.REPORTER);
-
- ticket = new Ticket();
- ticket.setId(10L);
- ticket.setReporter(reporter);
- ticket.setInvestigator(investigator);
- ticket.setReporterToken("valid-token");
-
- file = mock(MultipartFile.class);
+ @Mock private MinioStorageService storageService;
+ @Mock private AttachmentRepository attachmentRepository;
+ @Mock private TicketRepository ticketRepository;
+ @Mock private AuditService auditService;
+ @Mock private SecurityUtils securityUtils;
+
+ @InjectMocks private AttachmentService attachmentService;
+
+ private Ticket ticket;
+ private User admin;
+ private User investigator;
+ private User reporter;
+ private MultipartFile file;
+
+ @BeforeEach
+ void setUp() {
+ admin = new User();
+ admin.setId(100L);
+ admin.setRole(Role.ADMIN);
+
+ investigator = new User();
+ investigator.setId(200L);
+ investigator.setRole(Role.INVESTIGATOR);
+
+ reporter = new User();
+ reporter.setId(300L);
+ reporter.setRole(Role.REPORTER);
+
+ ticket = new Ticket();
+ ticket.setId(10L);
+ ticket.setReporter(reporter);
+ ticket.setInvestigator(investigator);
+ ticket.setReporterToken("valid-token");
+
+ file = mock(MultipartFile.class);
+ }
+
+ @Nested
+ @DisplayName("uploadToTicket tests")
+ class UploadToTicketTest {
+
+ @Test
+ @DisplayName("Authenticated reporter uploads to valid ticket")
+ void authenticatedReporter_withValidTicket_shouldUploadSuccessfully() throws Exception {
+ when(ticketRepository.findById(10L)).thenReturn(Optional.of(ticket));
+ when(storageService.upload(file)).thenReturn("s3-key");
+ when(file.getOriginalFilename()).thenReturn("report.pdf");
+
+ Attachment result = attachmentService.uploadToTicket(10L, file, reporter, null);
+
+ assertThat(result.getFileName()).isEqualTo("report.pdf");
+ assertThat(result.getS3Key()).isEqualTo("s3-key");
+ assertThat(result.getUploadedBy()).isSameAs(reporter);
+ verify(attachmentRepository).save(any(Attachment.class));
+ }
+
+ @Test
+ @DisplayName("Anonymous reporter uploads to valid ticket")
+ void anonymousReporter_withValidToken_shouldUploadSuccessfully() throws Exception {
+ when(ticketRepository.findByReporterToken("valid-token")).thenReturn(Optional.of(ticket));
+ when(storageService.upload(file)).thenReturn("s3-key");
+ when(file.getOriginalFilename()).thenReturn("evidence.pdf");
+
+ Attachment result = attachmentService.uploadToTicket(10L, file, null, "valid-token");
+
+ assertThat(result.getFileName()).isEqualTo("evidence.pdf");
+ verify(attachmentRepository).save(any(Attachment.class));
+ }
+
+ @Test
+ @DisplayName("Anonymous reporter with no token should throw Unauthorized")
+ void anonymousReporter_withMissingToken_throwsUnauthorized() {
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () -> attachmentService.uploadToTicket(10L, file, null, null));
+
+ assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
+ verifyNoInteractions(storageService);
+ }
+
+ @Test
+ @DisplayName("Anonymous reporter with blank token should throw Unauthorized")
+ void anonymousReporter_withBlankToken_throwsUnauthorized() {
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () -> attachmentService.uploadToTicket(10L, file, null, " "));
+
+ assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
+ verifyNoInteractions(storageService);
+ }
+
+ @Test
+ @DisplayName("Ticket not found should throw Not Found")
+ void ticketNotFound_throwsNotFound() {
+ when(ticketRepository.findById(11L)).thenReturn(Optional.empty());
+
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () -> attachmentService.uploadToTicket(11L, file, admin, null));
+
+ assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
+ }
+
+ @Test
+ @DisplayName("Null file name should fall back to default name")
+ void nullFileName_fallsBackToDefaultName() throws Exception {
+ when(ticketRepository.findById(10L)).thenReturn(Optional.of(ticket));
+ when(storageService.upload(file)).thenReturn("s3-key");
+ when(file.getOriginalFilename()).thenReturn(null);
+
+ Attachment result = attachmentService.uploadToTicket(10L, file, admin, null);
+
+ assertThat(result.getS3Key()).isEqualTo("s3-key");
+ assertThat(result.getFileName()).isEqualTo("file");
+ verify(attachmentRepository).save(any(Attachment.class));
+ }
+
+ @Test
+ @DisplayName("When save fails uploaded object should be deleted from storage")
+ void whenSaveFails_deletesUploadedObjectFromStorage() throws Exception {
+ when(ticketRepository.findById(10L)).thenReturn(Optional.of(ticket));
+ when(storageService.upload(file)).thenReturn("s3-key");
+ when(file.getOriginalFilename()).thenReturn("doc.pdf");
+ doThrow(new RuntimeException("DB down")).when(attachmentRepository).save(any());
+
+ assertThrows(
+ RuntimeException.class, () -> attachmentService.uploadToTicket(10L, file, admin, null));
+
+ verify(storageService).delete("s3-key");
+ }
+ }
+
+ @Nested
+ @DisplayName("checkAccess tests")
+ class CheckAccessTest {
+
+ @Test
+ @DisplayName("Admin always allowed")
+ void admin_alwaysAllowed() throws Exception {
+ when(ticketRepository.findById(10L)).thenReturn(Optional.of(ticket));
+ when(storageService.upload(file)).thenReturn("s3-key");
+ when(file.getOriginalFilename()).thenReturn("file.pdf");
+
+ assertDoesNotThrow(() -> attachmentService.uploadToTicket(10L, file, admin, null));
+ }
+
+ @Test
+ @DisplayName("Assigned investigator allowed")
+ void assignedInvestigator_allowed() throws Exception {
+ when(ticketRepository.findById(10L)).thenReturn(Optional.of(ticket));
+ when(storageService.upload(file)).thenReturn("s3-key");
+ when(file.getOriginalFilename()).thenReturn("f.pdf");
+
+ assertDoesNotThrow(() -> attachmentService.uploadToTicket(10L, file, investigator, null));
+ }
+
+ @Test
+ @DisplayName("Unassigned investigator denied")
+ void unassignedInvestigator_throwsForbidden() {
+ User otherInvestigator = new User();
+ otherInvestigator.setId(201L);
+ otherInvestigator.setRole(Role.INVESTIGATOR);
+
+ when(ticketRepository.findById(10L)).thenReturn(Optional.of(ticket));
+
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () -> attachmentService.uploadToTicket(10L, file, otherInvestigator, null));
+
+ assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
+ }
+
+ @Test
+ @DisplayName("Reporter who owns ticket allowed")
+ void reporterWhoOwnsTicket_allowed() throws Exception {
+ when(ticketRepository.findById(10L)).thenReturn(Optional.of(ticket));
+ when(storageService.upload(file)).thenReturn("s3-key");
+ when(file.getOriginalFilename()).thenReturn("f.pdf");
+
+ assertDoesNotThrow(() -> attachmentService.uploadToTicket(10L, file, reporter, null));
+ }
+
+ @Test
+ @DisplayName("Reporter who does not own ticket denied")
+ void reporterWhoDoesNotOwnTicket_throwsForbidden() {
+ User otherReporter = new User();
+ otherReporter.setId(301L);
+ otherReporter.setRole(Role.REPORTER);
+
+ when(ticketRepository.findById(10L)).thenReturn(Optional.of(ticket));
+
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () -> attachmentService.uploadToTicket(10L, file, otherReporter, null));
+
+ assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
}
- @Nested
- @DisplayName("uploadToTicket tests")
- class UploadToTicketTest {
-
- @Test
- @DisplayName("Authenticated reporter uploads to valid ticket")
- void authenticatedReporter_withValidTicket_shouldUploadSuccessfully() throws Exception {
- when(ticketRepository.findById(10L)).thenReturn(Optional.of(ticket));
- when(storageService.upload(file)).thenReturn("s3-key");
- when(file.getOriginalFilename()).thenReturn("report.pdf");
-
- Attachment result = attachmentService.uploadToTicket(10L, file, reporter, null);
-
- assertThat(result.getFileName()).isEqualTo("report.pdf");
- assertThat(result.getS3Key()).isEqualTo("s3-key");
- assertThat(result.getUploadedBy()).isSameAs(reporter);
- verify(attachmentRepository).save(any(Attachment.class));
- }
-
- @Test
- @DisplayName("Anonymous reporter uploads to valid ticket")
- void anonymousReporter_withValidToken_shouldUploadSuccessfully() throws Exception {
- when(ticketRepository.findByReporterToken("valid-token")).thenReturn(Optional.of(ticket));
- when(storageService.upload(file)).thenReturn("s3-key");
- when(file.getOriginalFilename()).thenReturn("evidence.pdf");
-
- Attachment result = attachmentService.uploadToTicket(10L, file, null, "valid-token");
-
- assertThat(result.getFileName()).isEqualTo("evidence.pdf");
- verify(attachmentRepository).save(any(Attachment.class));
- }
-
- @Test
- @DisplayName("Anonymous reporter with no token should throw Unauthorized")
- void anonymousReporter_withMissingToken_throwsUnauthorized() {
- ResponseStatusException ex = assertThrows(ResponseStatusException.class,
- () -> attachmentService.uploadToTicket(10L, file, null, null));
-
- assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
- verifyNoInteractions(storageService);
- }
-
- @Test
- @DisplayName("Anonymous reporter with blank token should throw Unauthorized")
- void anonymousReporter_withBlankToken_throwsUnauthorized() {
- ResponseStatusException ex = assertThrows(ResponseStatusException.class,
- () -> attachmentService.uploadToTicket(10L, file, null, " "));
-
- assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
- verifyNoInteractions(storageService);
- }
-
- @Test
- @DisplayName("Ticket not found should throw Not Found")
- void ticketNotFound_throwsNotFound() {
- when(ticketRepository.findById(11L)).thenReturn(Optional.empty());
-
- ResponseStatusException ex = assertThrows(ResponseStatusException.class,
- () -> attachmentService.uploadToTicket(11L, file, admin, null));
-
- assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
- }
-
- @Test
- @DisplayName("Null file name should fall back to default name")
- void nullFileName_fallsBackToDefaultName() throws Exception {
- when(ticketRepository.findById(10L)).thenReturn(Optional.of(ticket));
- when(storageService.upload(file)).thenReturn("s3-key");
- when(file.getOriginalFilename()).thenReturn(null);
-
- Attachment result = attachmentService.uploadToTicket(10L, file, admin, null);
-
- assertThat(result.getS3Key()).isEqualTo("s3-key");
- assertThat(result.getFileName()).isEqualTo("file");
- verify(attachmentRepository).save(any(Attachment.class));
- }
-
- @Test
- @DisplayName("When save fails uploaded object should be deleted from storage")
- void whenSaveFails_deletesUploadedObjectFromStorage() throws Exception {
- when(ticketRepository.findById(10L)).thenReturn(Optional.of(ticket));
- when(storageService.upload(file)).thenReturn("s3-key");
- when(file.getOriginalFilename()).thenReturn("doc.pdf");
- doThrow(new RuntimeException("DB down")).when(attachmentRepository).save(any());
-
- assertThrows(RuntimeException.class,
- () -> attachmentService.uploadToTicket(10L, file, admin, null));
-
- verify(storageService).delete("s3-key");
- }
+ @Test
+ @DisplayName("Anonymous reporter with valid token allowed")
+ void anonymousReporter_withCorrectToken_allowed() throws Exception {
+ when(ticketRepository.findByReporterToken("valid-token")).thenReturn(Optional.of(ticket));
+ when(storageService.upload(file)).thenReturn("s3-key");
+ when(file.getOriginalFilename()).thenReturn("f.pdf");
+
+ assertDoesNotThrow(() -> attachmentService.uploadToTicket(10L, file, null, "valid-token"));
}
- @Nested
- @DisplayName("checkAccess tests")
- class CheckAccessTest {
-
- @Test
- @DisplayName("Admin always allowed")
- void admin_alwaysAllowed() throws Exception {
- when(ticketRepository.findById(10L)).thenReturn(Optional.of(ticket));
- when(storageService.upload(file)).thenReturn("s3-key");
- when(file.getOriginalFilename()).thenReturn("file.pdf");
-
- assertDoesNotThrow(
- () -> attachmentService.uploadToTicket(10L, file, admin, null));
- }
-
- @Test
- @DisplayName("Assigned investigator allowed")
- void assignedInvestigator_allowed() throws Exception {
- when(ticketRepository.findById(10L)).thenReturn(Optional.of(ticket));
- when(storageService.upload(file)).thenReturn("s3-key");
- when(file.getOriginalFilename()).thenReturn("f.pdf");
-
- assertDoesNotThrow(() ->
- attachmentService.uploadToTicket(10L, file, investigator, null));
- }
-
- @Test
- @DisplayName("Unassigned investigator denied")
- void unassignedInvestigator_throwsForbidden() {
- User otherInvestigator = new User();
- otherInvestigator.setId(201L);
- otherInvestigator.setRole(Role.INVESTIGATOR);
-
- when(ticketRepository.findById(10L)).thenReturn(Optional.of(ticket));
-
- ResponseStatusException ex = assertThrows(ResponseStatusException.class,
- () -> attachmentService.uploadToTicket(10L, file, otherInvestigator, null));
-
- assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
- }
-
- @Test
- @DisplayName("Reporter who owns ticket allowed")
- void reporterWhoOwnsTicket_allowed() throws Exception {
- when(ticketRepository.findById(10L)).thenReturn(Optional.of(ticket));
- when(storageService.upload(file)).thenReturn("s3-key");
- when(file.getOriginalFilename()).thenReturn("f.pdf");
-
- assertDoesNotThrow(() ->
- attachmentService.uploadToTicket(10L, file, reporter, null));
- }
-
- @Test
- @DisplayName("Reporter who does not own ticket denied")
- void reporterWhoDoesNotOwnTicket_throwsForbidden() {
- User otherReporter = new User();
- otherReporter.setId(301L);
- otherReporter.setRole(Role.REPORTER);
-
- when(ticketRepository.findById(10L)).thenReturn(Optional.of(ticket));
-
- ResponseStatusException ex = assertThrows(ResponseStatusException.class,
- () -> attachmentService.uploadToTicket(10L, file, otherReporter, null));
-
- assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
- }
-
- @Test
- @DisplayName("Anonymous reporter with valid token allowed")
- void anonymousReporter_withCorrectToken_allowed() throws Exception {
- when(ticketRepository.findByReporterToken("valid-token")).thenReturn(Optional.of(ticket));
- when(storageService.upload(file)).thenReturn("s3-key");
- when(file.getOriginalFilename()).thenReturn("f.pdf");
-
- assertDoesNotThrow(() ->
- attachmentService.uploadToTicket(10L, file, null, "valid-token"));
- }
-
- @Test
- @DisplayName("Anonymous reporter with invalid token should return Not Found")
- void anonymousReporter_withWrongToken_throwsNotFound() {
- when(ticketRepository.findByReporterToken("wrong-token")).thenReturn(Optional.empty());
-
- ResponseStatusException ex = assertThrows(ResponseStatusException.class,
- () -> attachmentService.uploadToTicket(10L, file, null, "wrong-token"));
-
- assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
- }
+ @Test
+ @DisplayName("Anonymous reporter with invalid token should return Not Found")
+ void anonymousReporter_withWrongToken_throwsNotFound() {
+ when(ticketRepository.findByReporterToken("wrong-token")).thenReturn(Optional.empty());
+
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () -> attachmentService.uploadToTicket(10L, file, null, "wrong-token"));
+
+ assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
-}
\ No newline at end of file
+ }
+}
diff --git a/src/test/java/org/example/alfs/services/AuthServiceTest.java b/src/test/java/org/example/alfs/services/AuthServiceTest.java
index 09ca628..416a4a5 100644
--- a/src/test/java/org/example/alfs/services/AuthServiceTest.java
+++ b/src/test/java/org/example/alfs/services/AuthServiceTest.java
@@ -1,5 +1,11 @@
package org.example.alfs.services;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Optional;
import org.example.alfs.dto.auth.SignupRequestDTO;
import org.example.alfs.entities.User;
import org.example.alfs.enums.Role;
@@ -16,127 +22,118 @@
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.server.ResponseStatusException;
-import java.util.Optional;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
@ExtendWith(MockitoExtension.class)
class AuthServiceTest {
- @Mock
- private UserRepository userRepository;
- @Mock
- private PasswordEncoder passwordEncoder;
-
- @InjectMocks
- private AuthService authService;
-
- @Nested
- @DisplayName("Login tests")
- class Login {
-
- @Test
- @DisplayName("User not found should throw Unauthorized")
- void whenUserNotFound_throwsUnauthorized() {
- // Arrange
- when(userRepository.findByUsername("no-user"))
- .thenReturn(Optional.empty());
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class,
- () -> authService.login("no-user", "password"));
-
- // Assert
- assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
- }
-
- @Test
- @DisplayName("Wrong password should throw Unauthorized")
- void whenWrongPassword_throwsUnauthorized() {
- // Arrange
- User user = new User();
- user.setPasswordHash("hashed-password");
-
- when(userRepository.findByUsername("username")).thenReturn(Optional.of(user));
- when(passwordEncoder.matches("wrong-password", "hashed-password")).thenReturn(false);
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class,
- () -> authService.login("username", "wrong-password"));
-
- // Assert
- assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
- }
-
- @Test
- @DisplayName("Correct credentials should return user")
- void whenCredentialsCorrect_returnsUser() {
- // Arrange
- User user = new User();
- user.setUsername("username");
- user.setPasswordHash("hashed-password");
-
- when(userRepository.findByUsername("username")).thenReturn(Optional.of(user));
- when(passwordEncoder.matches("correct-password", "hashed-password")).thenReturn(true);
-
- // Act
- User result = authService.login("username", "correct-password");
-
- // Assert
- assertThat(result).isSameAs(user);
- assertThat(result.getUsername()).isEqualTo("username");
- assertThat(result.getPasswordHash()).isEqualTo("hashed-password");
- }
+ @Mock private UserRepository userRepository;
+ @Mock private PasswordEncoder passwordEncoder;
+
+ @InjectMocks private AuthService authService;
+
+ @Nested
+ @DisplayName("Login tests")
+ class Login {
+
+ @Test
+ @DisplayName("User not found should throw Unauthorized")
+ void whenUserNotFound_throwsUnauthorized() {
+ // Arrange
+ when(userRepository.findByUsername("no-user")).thenReturn(Optional.empty());
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class, () -> authService.login("no-user", "password"));
+
+ // Assert
+ assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
+ }
+
+ @Test
+ @DisplayName("Wrong password should throw Unauthorized")
+ void whenWrongPassword_throwsUnauthorized() {
+ // Arrange
+ User user = new User();
+ user.setPasswordHash("hashed-password");
+
+ when(userRepository.findByUsername("username")).thenReturn(Optional.of(user));
+ when(passwordEncoder.matches("wrong-password", "hashed-password")).thenReturn(false);
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class, () -> authService.login("username", "wrong-password"));
+
+ // Assert
+ assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
+ }
+
+ @Test
+ @DisplayName("Correct credentials should return user")
+ void whenCredentialsCorrect_returnsUser() {
+ // Arrange
+ User user = new User();
+ user.setUsername("username");
+ user.setPasswordHash("hashed-password");
+
+ when(userRepository.findByUsername("username")).thenReturn(Optional.of(user));
+ when(passwordEncoder.matches("correct-password", "hashed-password")).thenReturn(true);
+
+ // Act
+ User result = authService.login("username", "correct-password");
+
+ // Assert
+ assertThat(result).isSameAs(user);
+ assertThat(result.getUsername()).isEqualTo("username");
+ assertThat(result.getPasswordHash()).isEqualTo("hashed-password");
}
+ }
+
+ @Nested
+ @DisplayName("Signup tests")
+ class Signup {
+
+ @Test
+ @DisplayName("Username already taken should throw Bad Request")
+ void whenUsernameTaken_throwsBadRequest() {
+ // Arrange
+ when(userRepository.findByUsername("username")).thenReturn(Optional.of(new User()));
+
+ SignupRequestDTO request = new SignupRequestDTO();
+ request.setUsername("username");
+ request.setPassword("password");
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(ResponseStatusException.class, () -> authService.signup(request));
+
+ // Assert
+ assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(ex.getReason()).isEqualTo("Username already exists");
+ }
+
+ @Test
+ @DisplayName("Username available should save user with hashed password and reporter role")
+ void whenUsernameAvailable_savesUserWithHashedPasswordAndReporterRole() {
+ // Arrange
+ when(userRepository.findByUsername("username")).thenReturn(Optional.empty());
+ when(passwordEncoder.encode("password")).thenReturn("hashed-password");
+
+ SignupRequestDTO request = new SignupRequestDTO();
+ request.setUsername("username");
+ request.setPassword("password");
+
+ authService.signup(request);
+
+ // Act
+ ArgumentCaptor captor = ArgumentCaptor.forClass(User.class);
- @Nested
- @DisplayName("Signup tests")
- class Signup {
-
- @Test
- @DisplayName("Username already taken should throw Bad Request")
- void whenUsernameTaken_throwsBadRequest() {
- // Arrange
- when(userRepository.findByUsername("username")).thenReturn(Optional.of(new User()));
-
- SignupRequestDTO request = new SignupRequestDTO();
- request.setUsername("username");
- request.setPassword("password");
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class,
- () -> authService.signup(request));
-
- // Assert
- assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
- assertThat(ex.getReason()).isEqualTo("Username already exists");
- }
-
- @Test
- @DisplayName("Username available should save user with hashed password and reporter role")
- void whenUsernameAvailable_savesUserWithHashedPasswordAndReporterRole() {
- // Arrange
- when(userRepository.findByUsername("username")).thenReturn(Optional.empty());
- when(passwordEncoder.encode("password")).thenReturn("hashed-password");
-
- SignupRequestDTO request = new SignupRequestDTO();
- request.setUsername("username");
- request.setPassword("password");
-
- authService.signup(request);
-
- // Act
- ArgumentCaptor captor = ArgumentCaptor.forClass(User.class);
-
- // Assert
- verify(userRepository).save(captor.capture());
- User saved = captor.getValue();
- assertThat(saved.getUsername()).isEqualTo("username");
- assertThat(saved.getPasswordHash()).isEqualTo("hashed-password");
- assertThat(saved.getRole()).isEqualTo(Role.REPORTER);
- }
+ // Assert
+ verify(userRepository).save(captor.capture());
+ User saved = captor.getValue();
+ assertThat(saved.getUsername()).isEqualTo("username");
+ assertThat(saved.getPasswordHash()).isEqualTo("hashed-password");
+ assertThat(saved.getRole()).isEqualTo(Role.REPORTER);
}
-}
\ No newline at end of file
+ }
+}
diff --git a/src/test/java/org/example/alfs/services/TicketCommentServiceTest.java b/src/test/java/org/example/alfs/services/TicketCommentServiceTest.java
index 3cc5ae9..0419619 100644
--- a/src/test/java/org/example/alfs/services/TicketCommentServiceTest.java
+++ b/src/test/java/org/example/alfs/services/TicketCommentServiceTest.java
@@ -1,5 +1,11 @@
package org.example.alfs.services;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+import java.util.List;
+import java.util.Optional;
import org.example.alfs.dto.comment.CommentCreateDTO;
import org.example.alfs.dto.comment.CommentViewDTO;
import org.example.alfs.entities.Ticket;
@@ -20,430 +26,440 @@
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
-import java.util.List;
-import java.util.Optional;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.*;
-
@DisplayName("TicketCommentService Test")
@ExtendWith(MockitoExtension.class)
class TicketCommentServiceTest {
- @Mock
- TicketRepository ticketRepository;
- @Mock
- TicketCommentRepository ticketCommentRepository;
- @Mock
- TicketCommentMapper ticketCommentMapper;
-
- @InjectMocks
- TicketCommentService ticketCommentService;
-
- private Ticket openTicketWithReporter(User reporter) {
- Ticket t = new Ticket();
- t.setId(1L);
- t.setStatus(TicketStatus.OPEN);
- t.setReporter(reporter);
- return t;
+ @Mock TicketRepository ticketRepository;
+ @Mock TicketCommentRepository ticketCommentRepository;
+ @Mock TicketCommentMapper ticketCommentMapper;
+
+ @InjectMocks TicketCommentService ticketCommentService;
+
+ private Ticket openTicketWithReporter(User reporter) {
+ Ticket t = new Ticket();
+ t.setId(1L);
+ t.setStatus(TicketStatus.OPEN);
+ t.setReporter(reporter);
+ return t;
+ }
+
+ private User adminUser() {
+ User u = new User();
+ u.setId(100L);
+ u.setRole(Role.ADMIN);
+ return u;
+ }
+
+ private User investigatorUser() {
+ User u = new User();
+ u.setId(200L);
+ u.setRole(Role.INVESTIGATOR);
+ return u;
+ }
+
+ private User reporterUser() {
+ User u = new User();
+ u.setId(300L);
+ u.setRole(Role.REPORTER);
+ return u;
+ }
+
+ private Ticket anonymousTicket(String token) {
+ Ticket t = new Ticket();
+ t.setId(400L);
+ t.setStatus(TicketStatus.OPEN);
+ t.setReporterToken(token);
+ return t;
+ }
+
+ private CommentCreateDTO dto(String message, boolean internalNote) {
+ CommentCreateDTO dto = new CommentCreateDTO();
+ dto.setMessage(message);
+ dto.setInternalNote(internalNote);
+ return dto;
+ }
+
+ @Nested
+ @DisplayName("addComment tests")
+ class AddCommentTests {
+
+ @Test
+ @DisplayName("Admin can add a public comment")
+ void admin_canAddPublicComment() {
+ // Arrange
+ User admin = adminUser();
+ Ticket ticket = openTicketWithReporter(reporterUser());
+ CommentViewDTO expected = new CommentViewDTO();
+
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+ when(ticketCommentRepository.save(any())).thenAnswer(i -> i.getArgument(0));
+ when(ticketCommentMapper.entityToViewDTO(any())).thenReturn(expected);
+
+ // Act
+ CommentViewDTO result = ticketCommentService.addComment(1L, dto("Hello", false), admin, null);
+
+ // Assert
+ assertSame(expected, result);
+ verify(ticketCommentRepository).save(any());
+ }
+
+ @Test
+ @DisplayName("Admin can add an internal note")
+ void admin_canAddInternalNote() {
+ // Arrange
+ User admin = adminUser();
+ Ticket ticket = openTicketWithReporter(reporterUser());
+
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+ when(ticketCommentRepository.save(any())).thenAnswer(i -> i.getArgument(0));
+ when(ticketCommentMapper.entityToViewDTO(any())).thenReturn(new CommentViewDTO());
+
+ // Act + Assert
+ assertDoesNotThrow(
+ () -> ticketCommentService.addComment(1L, dto("Internal", true), admin, null));
+ }
+
+ @Test
+ @DisplayName("Assigned investigator can add a comment")
+ void assignedInvestigator_canAddComment() {
+ // Arrange
+ User investigator = investigatorUser();
+ Ticket ticket = openTicketWithReporter(reporterUser());
+ ticket.setInvestigator(investigator);
+
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+ when(ticketCommentRepository.save(any())).thenAnswer(i -> i.getArgument(0));
+ when(ticketCommentMapper.entityToViewDTO(any())).thenReturn(new CommentViewDTO());
+
+ // Act + Assert
+ assertDoesNotThrow(
+ () -> ticketCommentService.addComment(1L, dto("Note", false), investigator, null));
+ }
+
+ @Test
+ @DisplayName("Assigned investigator can add an internal note")
+ void assignedInvestigator_canAddInternalNote() {
+ // Arrange
+ User investigator = investigatorUser();
+ Ticket ticket = openTicketWithReporter(reporterUser());
+ ticket.setInvestigator(investigator);
+
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+ when(ticketCommentRepository.save(any())).thenAnswer(i -> i.getArgument(0));
+ when(ticketCommentMapper.entityToViewDTO(any())).thenReturn(new CommentViewDTO());
+
+ // Act + Assert
+ assertDoesNotThrow(
+ () -> ticketCommentService.addComment(1L, dto("Internal", true), investigator, null));
+ }
+
+ @Test
+ @DisplayName("Unassigned investigator should be forbidden")
+ void unassignedInvestigator_shouldThrowForbidden() {
+ // Arrange
+ User investigator = investigatorUser();
+ Ticket ticket = openTicketWithReporter(reporterUser());
+
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () -> ticketCommentService.addComment(1L, dto("Note", false), investigator, null));
+
+ // Assert
+ assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
+ verify(ticketCommentRepository, never()).save(any());
+ }
+
+ @Test
+ @DisplayName("Reporter who owns the ticket can add a comment")
+ void owningReporter_canAddComment() {
+ // Arrange
+ User reporter = reporterUser();
+ Ticket ticket = openTicketWithReporter(reporter);
+
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+ when(ticketCommentRepository.save(any())).thenAnswer(i -> i.getArgument(0));
+ when(ticketCommentMapper.entityToViewDTO(any())).thenReturn(new CommentViewDTO());
+
+ // Act + Assert
+ assertDoesNotThrow(
+ () -> ticketCommentService.addComment(1L, dto("My comment", false), reporter, null));
+ }
+
+ @Test
+ @DisplayName("Reporter cannot create an internal note")
+ void reporter_cannotCreateInternalNote() {
+ // Arrange
+ User reporter = reporterUser();
+ Ticket ticket = openTicketWithReporter(reporter);
+
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () -> ticketCommentService.addComment(1L, dto("Secret", true), reporter, null));
+
+ // Assert
+ assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
+ verify(ticketCommentRepository, never()).save(any());
+ }
+
+ @Test
+ @DisplayName("Reporter who does not own the ticket should be forbidden")
+ void nonOwningReporter_shouldThrowForbidden() {
+ // Arrange
+ User reporter = reporterUser();
+
+ User otherReporter = new User();
+ otherReporter.setId(301L);
+ otherReporter.setRole(Role.REPORTER);
+
+ Ticket ticket = openTicketWithReporter(otherReporter);
+
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () -> ticketCommentService.addComment(1L, dto("Note", false), reporter, null));
+
+ // Assert
+ assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
+ verify(ticketCommentRepository, never()).save(any());
+ }
+
+ @Test
+ @DisplayName("Anonymous user with valid token can add a comment")
+ void anonymous_withValidToken_canAddComment() {
+ // Arrange
+ String token = "valid-token";
+ Ticket ticket = anonymousTicket(token);
+
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+ when(ticketCommentRepository.save(any())).thenAnswer(i -> i.getArgument(0));
+ when(ticketCommentMapper.entityToViewDTO(any())).thenReturn(new CommentViewDTO());
+
+ // Act + Assert
+ assertDoesNotThrow(
+ () -> ticketCommentService.addComment(1L, dto("Anonymous comment", false), null, token));
+ }
+
+ @Test
+ @DisplayName("Anonymous user cannot create an internal note")
+ void anonymous_cannotCreateInternalNote() {
+ // Arrange
+ String token = "valid-token";
+ Ticket ticket = anonymousTicket(token);
+
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () -> ticketCommentService.addComment(1L, dto("Secret", true), null, token));
+
+ // Assert
+ assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
+ verify(ticketCommentRepository, never()).save(any());
+ }
+
+ @Test
+ @DisplayName("Anonymous user with wrong token should be forbidden")
+ void anonymous_withWrongToken_shouldThrowForbidden() {
+ // Arrange
+ Ticket ticket = anonymousTicket("correct-token");
+
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () -> ticketCommentService.addComment(1L, dto("Note", false), null, "wrong-token"));
+
+ // Assert
+ assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
+ verify(ticketCommentRepository, never()).save(any());
+ }
+
+ @Test
+ @DisplayName("Anonymous user with no token should be forbidden")
+ void anonymous_withNoToken_shouldThrowForbidden() {
+ // Arrange
+ Ticket ticket = anonymousTicket("valid-token");
+
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () -> ticketCommentService.addComment(1L, dto("Note", false), null, null));
+
+ // Assert
+ assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
+ verify(ticketCommentRepository, never()).save(any());
+ }
+
+ @Test
+ @DisplayName("Should throw Not Found when ticket does not exist")
+ void addComment_ticketNotFound_shouldThrowNotFound() {
+ // Arrange
+ when(ticketRepository.findById(1L)).thenReturn(Optional.empty());
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () -> ticketCommentService.addComment(1L, dto("Hello", false), adminUser(), null));
+
+ // Assert
+ assertEquals(HttpStatus.NOT_FOUND, ex.getStatusCode());
+ verify(ticketCommentRepository, never()).save(any());
}
+ }
+
+ @Nested
+ @DisplayName("getComments tests")
+ class GetCommentsTests {
+
+ @Test
+ @DisplayName("Admin sees all comments including internal notes")
+ void admin_seesAllComments() {
+ // Arrange
+ User admin = adminUser();
+ Ticket ticket = openTicketWithReporter(reporterUser());
- private User adminUser() {
- User u = new User();
- u.setId(100L);
- u.setRole(Role.ADMIN);
- return u;
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+ when(ticketCommentRepository.findByTicketIdOrderByCreatedAtAsc(1L)).thenReturn(List.of());
+
+ // Act
+ ticketCommentService.getComments(1L, admin, null);
+
+ // Assert
+ verify(ticketCommentRepository).findByTicketIdOrderByCreatedAtAsc(1L);
+ verify(ticketCommentRepository, never())
+ .findByTicketIdAndInternalNoteFalseOrderByCreatedAtAsc(any());
}
- private User investigatorUser() {
- User u = new User();
- u.setId(200L);
- u.setRole(Role.INVESTIGATOR);
- return u;
+ @Test
+ @DisplayName("Assigned investigator sees all comments including internal notes")
+ void assignedInvestigator_seesAllComments() {
+ // Arrange
+ User investigator = investigatorUser();
+ Ticket ticket = openTicketWithReporter(reporterUser());
+ ticket.setInvestigator(investigator);
+
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+ when(ticketCommentRepository.findByTicketIdOrderByCreatedAtAsc(1L)).thenReturn(List.of());
+
+ // Act
+ ticketCommentService.getComments(1L, investigator, null);
+
+ // Assert
+ verify(ticketCommentRepository).findByTicketIdOrderByCreatedAtAsc(1L);
}
- private User reporterUser() {
- User u = new User();
- u.setId(300L);
- u.setRole(Role.REPORTER);
- return u;
+ @Test
+ @DisplayName("Reporter only sees public comments")
+ void reporter_seesOnlyPublicComments() {
+ // Arrange
+ User reporter = reporterUser();
+ Ticket ticket = openTicketWithReporter(reporter);
+
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+ when(ticketCommentRepository.findByTicketIdAndInternalNoteFalseOrderByCreatedAtAsc(1L))
+ .thenReturn(List.of());
+
+ // Act
+ ticketCommentService.getComments(1L, reporter, null);
+
+ // Assert
+ verify(ticketCommentRepository).findByTicketIdAndInternalNoteFalseOrderByCreatedAtAsc(1L);
+ verify(ticketCommentRepository, never()).findByTicketIdOrderByCreatedAtAsc(any());
}
- private Ticket anonymousTicket(String token) {
- Ticket t = new Ticket();
- t.setId(400L);
- t.setStatus(TicketStatus.OPEN);
- t.setReporterToken(token);
- return t;
+ @Test
+ @DisplayName("Anonymous reporter with valid token only sees public comments")
+ void anonymous_withValidToken_seesOnlyPublicComments() {
+ // Arrange
+ String token = "valid-token";
+ Ticket ticket = anonymousTicket(token);
+
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+ when(ticketCommentRepository.findByTicketIdAndInternalNoteFalseOrderByCreatedAtAsc(1L))
+ .thenReturn(List.of());
+
+ // Act
+ ticketCommentService.getComments(1L, null, token);
+
+ // Assert
+ verify(ticketCommentRepository).findByTicketIdAndInternalNoteFalseOrderByCreatedAtAsc(1L);
+ verify(ticketCommentRepository, never()).findByTicketIdOrderByCreatedAtAsc(any());
}
- private CommentCreateDTO dto(String message, boolean internalNote) {
- CommentCreateDTO dto = new CommentCreateDTO();
- dto.setMessage(message);
- dto.setInternalNote(internalNote);
- return dto;
+ @Test
+ @DisplayName("Anonymous reporter with wrong token should be forbidden")
+ void anonymous_withWrongToken_shouldThrowForbidden() {
+ // Arrange
+ Ticket ticket = anonymousTicket("correct-token");
+
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () -> ticketCommentService.getComments(1L, null, "wrong-token"));
+
+ // Assert
+ assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
}
- @Nested
- @DisplayName("addComment tests")
- class AddCommentTests {
-
- @Test
- @DisplayName("Admin can add a public comment")
- void admin_canAddPublicComment() {
- // Arrange
- User admin = adminUser();
- Ticket ticket = openTicketWithReporter(reporterUser());
- CommentViewDTO expected = new CommentViewDTO();
-
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
- when(ticketCommentRepository.save(any())).thenAnswer(i -> i.getArgument(0));
- when(ticketCommentMapper.entityToViewDTO(any())).thenReturn(expected);
-
- // Act
- CommentViewDTO result = ticketCommentService.addComment(1L, dto("Hello", false), admin, null);
-
- // Assert
- assertSame(expected, result);
- verify(ticketCommentRepository).save(any());
- }
-
- @Test
- @DisplayName("Admin can add an internal note")
- void admin_canAddInternalNote() {
- // Arrange
- User admin = adminUser();
- Ticket ticket = openTicketWithReporter(reporterUser());
-
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
- when(ticketCommentRepository.save(any())).thenAnswer(i -> i.getArgument(0));
- when(ticketCommentMapper.entityToViewDTO(any())).thenReturn(new CommentViewDTO());
-
- // Act + Assert
- assertDoesNotThrow(() ->
- ticketCommentService.addComment(1L, dto("Internal", true), admin, null));
- }
-
- @Test
- @DisplayName("Assigned investigator can add a comment")
- void assignedInvestigator_canAddComment() {
- // Arrange
- User investigator = investigatorUser();
- Ticket ticket = openTicketWithReporter(reporterUser());
- ticket.setInvestigator(investigator);
-
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
- when(ticketCommentRepository.save(any())).thenAnswer(i -> i.getArgument(0));
- when(ticketCommentMapper.entityToViewDTO(any())).thenReturn(new CommentViewDTO());
-
- // Act + Assert
- assertDoesNotThrow(() ->
- ticketCommentService.addComment(1L, dto("Note", false), investigator, null));
- }
-
- @Test
- @DisplayName("Assigned investigator can add an internal note")
- void assignedInvestigator_canAddInternalNote() {
- // Arrange
- User investigator = investigatorUser();
- Ticket ticket = openTicketWithReporter(reporterUser());
- ticket.setInvestigator(investigator);
-
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
- when(ticketCommentRepository.save(any())).thenAnswer(i -> i.getArgument(0));
- when(ticketCommentMapper.entityToViewDTO(any())).thenReturn(new CommentViewDTO());
-
- // Act + Assert
- assertDoesNotThrow(() ->
- ticketCommentService.addComment(1L, dto("Internal", true), investigator, null));
- }
-
- @Test
- @DisplayName("Unassigned investigator should be forbidden")
- void unassignedInvestigator_shouldThrowForbidden() {
- // Arrange
- User investigator = investigatorUser();
- Ticket ticket = openTicketWithReporter(reporterUser());
-
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketCommentService.addComment(1L, dto("Note", false), investigator, null));
-
- // Assert
- assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
- verify(ticketCommentRepository, never()).save(any());
- }
-
- @Test
- @DisplayName("Reporter who owns the ticket can add a comment")
- void owningReporter_canAddComment() {
- // Arrange
- User reporter = reporterUser();
- Ticket ticket = openTicketWithReporter(reporter);
-
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
- when(ticketCommentRepository.save(any())).thenAnswer(i -> i.getArgument(0));
- when(ticketCommentMapper.entityToViewDTO(any())).thenReturn(new CommentViewDTO());
-
- // Act + Assert
- assertDoesNotThrow(() ->
- ticketCommentService.addComment(1L, dto("My comment", false), reporter, null));
- }
-
- @Test
- @DisplayName("Reporter cannot create an internal note")
- void reporter_cannotCreateInternalNote() {
- // Arrange
- User reporter = reporterUser();
- Ticket ticket = openTicketWithReporter(reporter);
-
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketCommentService.addComment(1L, dto("Secret", true), reporter, null));
-
- // Assert
- assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
- verify(ticketCommentRepository, never()).save(any());
- }
-
- @Test
- @DisplayName("Reporter who does not own the ticket should be forbidden")
- void nonOwningReporter_shouldThrowForbidden() {
- // Arrange
- User reporter = reporterUser();
-
- User otherReporter = new User();
- otherReporter.setId(301L);
- otherReporter.setRole(Role.REPORTER);
-
- Ticket ticket = openTicketWithReporter(otherReporter);
-
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketCommentService.addComment(1L, dto("Note", false), reporter, null));
-
- // Assert
- assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
- verify(ticketCommentRepository, never()).save(any());
- }
-
- @Test
- @DisplayName("Anonymous user with valid token can add a comment")
- void anonymous_withValidToken_canAddComment() {
- // Arrange
- String token = "valid-token";
- Ticket ticket = anonymousTicket(token);
-
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
- when(ticketCommentRepository.save(any())).thenAnswer(i -> i.getArgument(0));
- when(ticketCommentMapper.entityToViewDTO(any())).thenReturn(new CommentViewDTO());
-
- // Act + Assert
- assertDoesNotThrow(() ->
- ticketCommentService.addComment(1L, dto("Anonymous comment", false), null, token));
- }
-
- @Test
- @DisplayName("Anonymous user cannot create an internal note")
- void anonymous_cannotCreateInternalNote() {
- // Arrange
- String token = "valid-token";
- Ticket ticket = anonymousTicket(token);
-
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketCommentService.addComment(1L, dto("Secret", true), null, token));
-
- // Assert
- assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
- verify(ticketCommentRepository, never()).save(any());
- }
-
- @Test
- @DisplayName("Anonymous user with wrong token should be forbidden")
- void anonymous_withWrongToken_shouldThrowForbidden() {
- // Arrange
- Ticket ticket = anonymousTicket("correct-token");
-
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketCommentService.addComment(1L, dto("Note", false), null, "wrong-token"));
-
- // Assert
- assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
- verify(ticketCommentRepository, never()).save(any());
- }
-
- @Test
- @DisplayName("Anonymous user with no token should be forbidden")
- void anonymous_withNoToken_shouldThrowForbidden() {
- // Arrange
- Ticket ticket = anonymousTicket("valid-token");
-
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketCommentService.addComment(1L, dto("Note", false), null, null));
-
- // Assert
- assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
- verify(ticketCommentRepository, never()).save(any());
- }
-
- @Test
- @DisplayName("Should throw Not Found when ticket does not exist")
- void addComment_ticketNotFound_shouldThrowNotFound() {
- // Arrange
- when(ticketRepository.findById(1L)).thenReturn(Optional.empty());
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketCommentService.addComment(1L, dto("Hello", false), adminUser(), null));
-
- // Assert
- assertEquals(HttpStatus.NOT_FOUND, ex.getStatusCode());
- verify(ticketCommentRepository, never()).save(any());
- }
+ @Test
+ @DisplayName("Should throw Not Found when ticket does not exist")
+ void getComments_ticketNotFound_shouldThrowNotFound() {
+ // Arrange
+ when(ticketRepository.findById(1L)).thenReturn(Optional.empty());
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () -> ticketCommentService.getComments(1L, adminUser(), null));
+ // Assert
+ assertEquals(HttpStatus.NOT_FOUND, ex.getStatusCode());
}
- @Nested
- @DisplayName("getComments tests")
- class GetCommentsTests {
-
- @Test
- @DisplayName("Admin sees all comments including internal notes")
- void admin_seesAllComments() {
- // Arrange
- User admin = adminUser();
- Ticket ticket = openTicketWithReporter(reporterUser());
-
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
- when(ticketCommentRepository.findByTicketIdOrderByCreatedAtAsc(1L)).thenReturn(List.of());
-
- // Act
- ticketCommentService.getComments(1L, admin, null);
-
- // Assert
- verify(ticketCommentRepository).findByTicketIdOrderByCreatedAtAsc(1L);
- verify(ticketCommentRepository, never()).findByTicketIdAndInternalNoteFalseOrderByCreatedAtAsc(any());
- }
-
- @Test
- @DisplayName("Assigned investigator sees all comments including internal notes")
- void assignedInvestigator_seesAllComments() {
- // Arrange
- User investigator = investigatorUser();
- Ticket ticket = openTicketWithReporter(reporterUser());
- ticket.setInvestigator(investigator);
-
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
- when(ticketCommentRepository.findByTicketIdOrderByCreatedAtAsc(1L)).thenReturn(List.of());
-
- // Act
- ticketCommentService.getComments(1L, investigator, null);
-
- // Assert
- verify(ticketCommentRepository).findByTicketIdOrderByCreatedAtAsc(1L);
- }
-
- @Test
- @DisplayName("Reporter only sees public comments")
- void reporter_seesOnlyPublicComments() {
- // Arrange
- User reporter = reporterUser();
- Ticket ticket = openTicketWithReporter(reporter);
-
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
- when(ticketCommentRepository.findByTicketIdAndInternalNoteFalseOrderByCreatedAtAsc(1L)).thenReturn(List.of());
-
- // Act
- ticketCommentService.getComments(1L, reporter, null);
-
- // Assert
- verify(ticketCommentRepository).findByTicketIdAndInternalNoteFalseOrderByCreatedAtAsc(1L);
- verify(ticketCommentRepository, never()).findByTicketIdOrderByCreatedAtAsc(any());
- }
-
- @Test
- @DisplayName("Anonymous reporter with valid token only sees public comments")
- void anonymous_withValidToken_seesOnlyPublicComments() {
- // Arrange
- String token = "valid-token";
- Ticket ticket = anonymousTicket(token);
-
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
- when(ticketCommentRepository.findByTicketIdAndInternalNoteFalseOrderByCreatedAtAsc(1L)).thenReturn(List.of());
-
- // Act
- ticketCommentService.getComments(1L, null, token);
-
- // Assert
- verify(ticketCommentRepository).findByTicketIdAndInternalNoteFalseOrderByCreatedAtAsc(1L);
- verify(ticketCommentRepository, never()).findByTicketIdOrderByCreatedAtAsc(any());
- }
-
- @Test
- @DisplayName("Anonymous reporter with wrong token should be forbidden")
- void anonymous_withWrongToken_shouldThrowForbidden() {
- // Arrange
- Ticket ticket = anonymousTicket("correct-token");
-
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketCommentService.getComments(1L, null, "wrong-token"));
-
- // Assert
- assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
- }
-
- @Test
- @DisplayName("Should throw Not Found when ticket does not exist")
- void getComments_ticketNotFound_shouldThrowNotFound() {
- // Arrange
- when(ticketRepository.findById(1L)).thenReturn(Optional.empty());
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketCommentService.getComments(1L, adminUser(), null));
-
- // Assert
- assertEquals(HttpStatus.NOT_FOUND, ex.getStatusCode());
- }
-
- @Test
- @DisplayName("Returned comments are mapped correctly")
- void getComments_returnsMappedDTOs() {
- // Arrange
- User admin = adminUser();
- Ticket ticket = openTicketWithReporter(reporterUser());
- TicketComment comment = new TicketComment();
- CommentViewDTO expected = new CommentViewDTO();
-
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
- when(ticketCommentRepository.findByTicketIdOrderByCreatedAtAsc(1L)).thenReturn(List.of(comment));
- when(ticketCommentMapper.entityToViewDTO(comment)).thenReturn(expected);
-
- // Act
- List result = ticketCommentService.getComments(1L, admin, null);
-
- // Assert
- assertEquals(1, result.size());
- assertSame(expected, result.getFirst());
- }
+ @Test
+ @DisplayName("Returned comments are mapped correctly")
+ void getComments_returnsMappedDTOs() {
+ // Arrange
+ User admin = adminUser();
+ Ticket ticket = openTicketWithReporter(reporterUser());
+ TicketComment comment = new TicketComment();
+ CommentViewDTO expected = new CommentViewDTO();
+
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+ when(ticketCommentRepository.findByTicketIdOrderByCreatedAtAsc(1L))
+ .thenReturn(List.of(comment));
+ when(ticketCommentMapper.entityToViewDTO(comment)).thenReturn(expected);
+
+ // Act
+ List result = ticketCommentService.getComments(1L, admin, null);
+
+ // Assert
+ assertEquals(1, result.size());
+ assertSame(expected, result.getFirst());
}
-}
\ 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 36b54d4..61c3b8a 100644
--- a/src/test/java/org/example/alfs/services/TicketServiceTest.java
+++ b/src/test/java/org/example/alfs/services/TicketServiceTest.java
@@ -1,5 +1,11 @@
package org.example.alfs.services;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+import java.util.List;
+import java.util.Optional;
import org.example.alfs.dto.ticket.TicketCreateDTO;
import org.example.alfs.dto.ticket.TicketViewDTO;
import org.example.alfs.entities.Ticket;
@@ -22,830 +28,845 @@
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
-import java.util.List;
-import java.util.Optional;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.*;
-
@DisplayName("TicketService Test")
@ExtendWith(MockitoExtension.class)
class TicketServiceTest {
- @Mock
- TicketRepository ticketRepository;
- @Mock
- TicketMapper ticketMapper;
- @Mock
- UserRepository userRepository;
- @Mock
- SecurityUtils securityUtils;
- @Mock
- AuditService auditService;
-
- @InjectMocks
- TicketService ticketService;
-
- private Ticket openTicket() {
- Ticket t = new Ticket();
- t.setId(1L);
- t.setStatus(TicketStatus.OPEN);
- return t;
- }
-
- private User adminUser() {
- User u = new User();
- u.setId(100L);
- u.setRole(Role.ADMIN);
- return u;
- }
-
- private User investigatorUser() {
- User u = new User();
- u.setId(200L);
- u.setRole(Role.INVESTIGATOR);
- return u;
- }
-
- private User reporterUser() {
- User u = new User();
- u.setId(300L);
- u.setRole(Role.REPORTER);
- return u;
- }
-
- @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
- @DisplayName("getTicketByToken tests")
- class GetTicketByTokenTests {
-
- @Test
- @DisplayName("Valid token should return ticket")
- void getTicketByToken_shouldReturnTicket() {
- // Arrange
- Ticket ticket = new Ticket();
- String token = "valid-token";
-
- when(ticketRepository.findByReporterToken(token)).thenReturn(Optional.of(ticket));
- when(ticketMapper.entityToViewDTO(any())).thenReturn(new TicketViewDTO());
-
- // Act + Assert
- assertDoesNotThrow(() -> ticketService.getTicketByToken(token));
- verify(ticketMapper).entityToViewDTO(ticket);
- }
-
- @Test
- @DisplayName("Invalid token should throw Not found")
- void getTicketByToken_invalidToken_shouldThrowNotFound() {
- // Arrange
- when(ticketRepository.findByReporterToken(any())).thenReturn(Optional.empty());
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketService.getTicketByToken("invalid-token"));
-
- // Assert
- assertEquals(HttpStatus.NOT_FOUND, ex.getStatusCode());
- }
- }
-
- @Nested
- @DisplayName("getTicketById tests")
- class GetTicketByIdTests {
-
- @Test
- @DisplayName("getTicketById should return ticket when found and access is granted")
- void getTicketById_shouldReturnTicket_whenFound() {
- // Arrange
- Ticket ticket = openTicket();
- User admin = adminUser();
- TicketViewDTO expected = new TicketViewDTO();
-
- when(securityUtils.getCurrentUser()).thenReturn(admin);
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
- when(ticketMapper.entityToViewDTO(ticket)).thenReturn(expected);
-
- // Act
- TicketViewDTO result = ticketService.getTicketById(1L);
-
- // Assert
- assertSame(expected, result);
- verify(ticketMapper).entityToViewDTO(ticket);
- }
-
- @Test
- @DisplayName("getTicketById should throw Not Found when ticket does not exist")
- void getTicketById_shouldThrow_whenNotFound() {
- // Arrange
- when(ticketRepository.findById(1L)).thenReturn(Optional.empty());
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketService.getTicketById(1L));
-
- // Assert
- assertEquals(HttpStatus.NOT_FOUND, ex.getStatusCode());
- }
- }
-
- @Nested
- @DisplayName("getMyTickets tests")
- class GetMyTicketsTests {
-
- @Test
- @DisplayName("Should return tickets for the current reporter")
- void getMyTickets_shouldReturnTicketsForCurrentUser() {
- // Arrange
- User reporter = reporterUser();
- Ticket ticket = openTicket();
- TicketViewDTO expected = new TicketViewDTO();
-
- when(securityUtils.getCurrentUser()).thenReturn(reporter);
- when(ticketRepository.findByReporterId(reporter.getId())).thenReturn(List.of(ticket));
- when(ticketMapper.entityToViewDTO(ticket)).thenReturn(expected);
-
- // Act
- List result = ticketService.getMyTickets();
-
- // Assert
- assertEquals(1, result.size());
- assertSame(expected, result.getFirst());
- verify(ticketRepository).findByReporterId(reporter.getId());
- }
-
- @Test
- @DisplayName("Should return empty list when reporter has no tickets")
- void getMyTickets_shouldReturnEmptyList_whenNoTickets() {
- // Arrange
- User reporter = reporterUser();
-
- when(securityUtils.getCurrentUser()).thenReturn(reporter);
- when(ticketRepository.findByReporterId(reporter.getId())).thenReturn(List.of());
-
- // Act
- List result = ticketService.getMyTickets();
-
- // Assert
- assertTrue(result.isEmpty());
- verify(ticketMapper, never()).entityToViewDTO(any());
- }
- }
-
- @Nested
- @DisplayName("getMyAssignedTickets tests")
- class GetMyAssignedTickets {
-
- @Test
- @DisplayName("Should return tickets for the current investigator")
- void getMyAssignedTickets_shouldReturnTicketsForCurrentUser() {
- // Arrange
- User investigator = investigatorUser();
- Ticket ticket = openTicket();
- TicketViewDTO expected = new TicketViewDTO();
-
- when(securityUtils.getCurrentUser()).thenReturn(investigator);
- when(ticketRepository.findByInvestigatorId(investigator.getId())).thenReturn(List.of(ticket));
- when(ticketMapper.entityToViewDTO(ticket)).thenReturn(expected);
-
- // Act
- List result = ticketService.getMyAssignedTickets();
-
- // Assert
- assertEquals(1, result.size());
- assertSame(expected, result.getFirst());
- verify(ticketRepository).findByInvestigatorId(investigator.getId());
- }
-
- @Test
- @DisplayName("Should return empty list when investigator has no tickets")
- void getMyAssignedTickets_shouldReturnEmptyList_whenNoTickets() {
- // Arrange
- User investigator = investigatorUser();
-
- when(securityUtils.getCurrentUser()).thenReturn(investigator);
- when(ticketRepository.findByInvestigatorId(investigator.getId())).thenReturn(List.of());
-
- // Act
- List result = ticketService.getMyAssignedTickets();
-
- // Assert
- assertTrue(result.isEmpty());
- verify(ticketMapper, never()).entityToViewDTO(any());
- }
- }
-
- @Nested
- @DisplayName("getTicketsByStatus tests")
- class GetTicketsByStatusTests {
-
- @Test
- @DisplayName("Admin can get tickets by status")
- void admin_canGetTicketsByStatus() {
- // Arrange
- when(securityUtils.getCurrentUser()).thenReturn(adminUser());
- when(ticketRepository.findByStatus(TicketStatus.OPEN)).thenReturn(List.of());
-
- // Act + Assert
- assertDoesNotThrow(() -> ticketService.getTicketsByStatus(TicketStatus.OPEN));
- }
-
- @Test
- @DisplayName("Non-admin cannot get tickets by status")
- void nonAdmin_cannotGetTicketsByStatus() {
- // Arrange
- when(securityUtils.getCurrentUser()).thenReturn(investigatorUser());
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketService.getTicketsByStatus(TicketStatus.OPEN));
-
- // Assert
- assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
- }
- }
-
- @Nested
- @DisplayName("getTicketsByStatusAndInvestigator tests")
- class GetTicketsByStatusAndInvestigatorTests {
-
- @Test
- @DisplayName("Admin can filter any investigator's tickets")
- void admin_canFilterAnyInvestigator() {
- // Arrange
- User admin = adminUser();
-
- when(securityUtils.getCurrentUser()).thenReturn(admin);
- when(ticketRepository.findByStatusAndInvestigatorId(any(), any())).thenReturn(List.of());
-
- // Act + Assert
- assertDoesNotThrow(() -> ticketService.getTicketsByStatusAndInvestigator(TicketStatus.OPEN, 200L));
- }
-
- @Test
- @DisplayName("Investigator can filter their own tickets")
- void investigator_canFilterOwnTickets() {
- // Arrange
- User investigator = investigatorUser();
-
- when(securityUtils.getCurrentUser()).thenReturn(investigator);
- when(ticketRepository.findByStatusAndInvestigatorId(any(), any())).thenReturn(List.of());
-
- // Act + Assert
- assertDoesNotThrow(() -> ticketService.getTicketsByStatusAndInvestigator(TicketStatus.IN_PROGRESS, 200L));
- }
-
- @Test
- @DisplayName("Investigator cannot filter other investigator's tickets")
- void investigator_cannotFilterOthersTickets() {
- // Arrange
- User investigator = investigatorUser();
-
- when(securityUtils.getCurrentUser()).thenReturn(investigator);
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketService.getTicketsByStatusAndInvestigator(TicketStatus.IN_PROGRESS, 201L));
-
- // Assert
- assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
- }
-
- @Test
- @DisplayName("Reporter should be forbidden from filtering by investigator ID")
- void reporter_cannotFilterTickets_byInvestigatorId() {
- // Arrange
- User reporter = reporterUser();
-
- when(securityUtils.getCurrentUser()).thenReturn(reporter);
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketService.getTicketsByStatusAndInvestigator(TicketStatus.IN_PROGRESS, 200L));
-
- // Assert
- assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
- }
- }
-
- @Nested
- @DisplayName("checkAccess tests")
- class CheckAccessTests {
-
- @Test
- @DisplayName("Admin should always have access")
- void admin_shouldHaveAccess() {
- // Arrange
- Ticket ticket = openTicket();
- User admin = adminUser();
-
- when(securityUtils.getCurrentUser()).thenReturn(admin);
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
- when(ticketMapper.entityToViewDTO(any())).thenReturn(new TicketViewDTO());
-
- // Act + Assert
- assertDoesNotThrow(() -> ticketService.getTicketById(1L));
- }
-
- @Test
- @DisplayName("Assigned investigator should have access")
- void investigator_shouldHaveAccessIfAssigned() {
- // Arrange
- Ticket ticket = openTicket();
- User investigator = investigatorUser();
- ticket.setInvestigator(investigator);
-
- when(securityUtils.getCurrentUser()).thenReturn(investigator);
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
- when(ticketMapper.entityToViewDTO(any())).thenReturn(new TicketViewDTO());
-
- // Act + Assert
- assertDoesNotThrow(() -> ticketService.getTicketById(1L));
- }
-
- @Test
- @DisplayName("Unassigned investigator should not have access")
- void unassignedInvestigator_shouldThrowForbidden() {
- // Arrange
- Ticket ticket = openTicket();
- User investigator = investigatorUser();
-
- User otherInvestigator = new User();
- otherInvestigator.setId(201L);
- otherInvestigator.setRole(Role.INVESTIGATOR);
- ticket.setInvestigator(otherInvestigator);
-
- when(securityUtils.getCurrentUser()).thenReturn(investigator);
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketService.getTicketById(1L));
-
- // Assert
- assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
- }
-
- @Test
- @DisplayName("Reporter who owns the ticket should have access")
- void owningReporter_shouldHaveAccess() {
- // Arrange
- Ticket ticket = openTicket();
- User reporter = reporterUser();
- ticket.setReporter(reporter);
-
- when(securityUtils.getCurrentUser()).thenReturn(reporter);
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
- when(ticketMapper.entityToViewDTO(any())).thenReturn(new TicketViewDTO());
-
- // Act + Assert
- assertDoesNotThrow(() -> ticketService.getTicketById(1L));
- }
-
- @Test
- @DisplayName("Reporter who does not own the ticket should not have access")
- void nonOwningReporter_shouldThrowForbidden() {
- // Arrange
- Ticket ticket = openTicket();
- User reporter = reporterUser();
-
- User otherReporter = new User();
- otherReporter.setId(301L);
- otherReporter.setRole(Role.REPORTER);
- ticket.setReporter(otherReporter);
-
- when(securityUtils.getCurrentUser()).thenReturn(reporter);
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketService.getTicketById(1L));
-
- // Assert
- assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
- }
-
- @Test
- @DisplayName("Investigator with null investigator set on ticket should be forbidden")
- void investigator_nullInvestigatorOnTicket_shouldThrowForbidden() {
- Ticket ticket = openTicket();
- User investigator = investigatorUser();
- ticket.setInvestigator(null);
-
- when(securityUtils.getCurrentUser()).thenReturn(investigator);
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
-
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketService.getTicketById(1L));
-
- assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
- }
- }
-
- @Nested
- @DisplayName("requireCurrentUser tests")
- class RequireCurrentUserTests {
-
- @Test
- @DisplayName("No authenticated user in security context should throw Unauthorized")
- void noAuthenticatedUser_shouldThrowUnauthorized() {
- // Arrange
- when(securityUtils.getCurrentUser())
- .thenThrow(new RuntimeException("No authenticated user in security context"));
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketService.getMyTickets());
-
- // Assert
- assertEquals(HttpStatus.UNAUTHORIZED, ex.getStatusCode());
- verify(ticketRepository, never()).findByReporterId(any());
- }
-
- @Test
- @DisplayName("Authenticated user not found in database should throw Unauthorized")
- void userNotFoundInDatabase_shouldThrowUnauthorized() {
- // Arrange
- when(securityUtils.getCurrentUser())
- .thenThrow(new RuntimeException("Authenticated user not found in database"));
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketService.getMyTickets());
-
- // Assert
- assertEquals(HttpStatus.UNAUTHORIZED, ex.getStatusCode());
- verify(ticketRepository, never()).findByReporterId(any());
- }
- }
-
- @Nested
- @DisplayName("updateTicketStatus tests")
- class UpdateTicketStatusTests {
-
- @Test
- @DisplayName("Valid transition should succeed")
- void validTransition_shouldSucceed() {
- // Arrange
- Ticket ticket = openTicket();
- User investigator = investigatorUser();
- ticket.setInvestigator(investigator);
-
- when(securityUtils.getCurrentUser()).thenReturn(investigator);
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
- when(ticketRepository.save(any())).thenReturn(ticket);
- when(ticketMapper.entityToViewDTO(any())).thenReturn(new TicketViewDTO());
-
- // Act
- ticketService.updateTicketStatus(1L, TicketStatus.IN_PROGRESS);
-
- // Assert
- assertEquals(TicketStatus.IN_PROGRESS, ticket.getStatus());
- verify(ticketRepository).save(ticket);
- }
-
- @Test
- @DisplayName("Invalid transition should throw Bad Request")
- void invalidTransition_shouldThrowBadRequest() {
- // Arrange
- Ticket ticket = openTicket();
- User admin = adminUser();
-
- when(securityUtils.getCurrentUser()).thenReturn(admin);
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketService.updateTicketStatus(1L, TicketStatus.CLOSED));
-
- // Assert
- assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode());
- verify(ticketRepository, never()).save(any());
- }
-
- @Test
- @DisplayName("Reporter should not be able to update status")
- void reporterStatusUpdate_shouldThrowForbidden() {
- // Arrange
- Ticket ticket = openTicket();
- User reporter = reporterUser();
-
- when(securityUtils.getCurrentUser()).thenReturn(reporter);
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketService.updateTicketStatus(1L, TicketStatus.CLOSED));
-
- // Assert
- assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
- verify(ticketRepository, never()).save(any());
- }
-
- @Test
- @DisplayName("Same status should return without changes")
- void sameStatus_shouldReturn() {
- //Arrange
- Ticket ticket = openTicket();
- User admin = adminUser();
-
- when(securityUtils.getCurrentUser()).thenReturn(admin);
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
- when(ticketMapper.entityToViewDTO(any())).thenReturn(new TicketViewDTO());
-
- // Act
- TicketViewDTO result = ticketService.updateTicketStatus(1L, TicketStatus.OPEN);
-
- // Assert
- assertNotNull(result);
- verify(ticketRepository, never()).save(any());
- }
-
- @Test
- @DisplayName("Status change without investigator should throw Bad Request")
- void statusChange_withoutInvestigator_shouldThrowBadRequest() {
- // Arrange
- Ticket ticket = openTicket();
- User admin = adminUser();
- ticket.setInvestigator(null);
-
- when(securityUtils.getCurrentUser()).thenReturn(admin);
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketService.updateTicketStatus(1L, TicketStatus.IN_PROGRESS));
-
- // Assert
- assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode());
- verify(ticketRepository, never()).save(any());
- }
-
- @Test
- @DisplayName("Investigator not assigned should be forbidden")
- void investigator_notAssigned_shouldBeForbidden() {
- // Arrange
- Ticket ticket = openTicket();
- User investigator = investigatorUser();
-
- User otherInvestigator = new User();
- otherInvestigator.setId(201L);
- otherInvestigator.setRole(Role.INVESTIGATOR);
- ticket.setInvestigator(otherInvestigator);
-
- when(securityUtils.getCurrentUser()).thenReturn(investigator);
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketService.updateTicketStatus(1L, TicketStatus.IN_PROGRESS));
-
- // Assert
- assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
- verify(ticketRepository, never()).save(any());
- }
- }
-
- @Nested
- @DisplayName("assignInvestigator tests")
- class AssignInvestigatorTests {
-
- @Test
- @DisplayName("Assigning investigator should succeed")
- void assignInvestigator_shouldSucceed() {
- // Arrange
- Ticket ticket = openTicket();
- User admin = adminUser();
- User investigator = investigatorUser();
-
- when(securityUtils.getCurrentUser()).thenReturn(admin);
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
- when(userRepository.findById(investigator.getId())).thenReturn(Optional.of(investigator));
- when(ticketRepository.save(any())).thenReturn(ticket);
- when(ticketMapper.entityToViewDTO(any())).thenReturn(new TicketViewDTO());
-
- // Act
- ticketService.assignInvestigator(1L, investigator.getId());
-
- // Assert
- assertEquals(investigator, ticket.getInvestigator());
- assertEquals(TicketStatus.IN_PROGRESS, ticket.getStatus());
- verify(ticketRepository).save(ticket);
- }
-
- @Test
- @DisplayName("Assigning investigator should throw Bad Request when investigatorId is null")
- void assignInvestigator_shouldThrowBadRequest_whenInvestigatorIsNull() {
- // Arrange + Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketService.assignInvestigator(1L, null));
-
- // Assert
- assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode());
- verify(ticketRepository, never()).save(any());
- }
-
- @Test
- @DisplayName("Assigning investigator should throw Conflict when ticket is already assigned")
- void assignInvestigator_shouldThrowConflict_whenTicketAlreadyAssigned() {
- // Arrange
- Ticket ticket = openTicket();
- User admin = adminUser();
- User investigator = investigatorUser();
- ticket.setInvestigator(investigator);
-
- when(securityUtils.getCurrentUser()).thenReturn(admin);
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketService.assignInvestigator(1L, investigator.getId()));
-
- // Assert
- assertEquals(HttpStatus.CONFLICT, ex.getStatusCode());
- verify(ticketRepository, never()).save(any());
- }
-
- @Test
- @DisplayName("Assigning investigator should throw Bad Request when ticket is not open")
- void assignInvestigator_shouldThrowBadRequest_whenIsNotOpen() {
- // Arrange
- Ticket ticket = openTicket();
- User admin = adminUser();
- User investigator = investigatorUser();
- ticket.setStatus(TicketStatus.CLOSED);
-
- when(securityUtils.getCurrentUser()).thenReturn(admin);
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketService.assignInvestigator(1L, investigator.getId()));
-
- // Assert
- assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode());
- verify(ticketRepository, never()).save(any());
- }
-
- @Test
- @DisplayName("Assigning investigator should throw Bad Request when user is not investigator")
- void assignInvestigator_shouldThrowBadRequest_whenUserIsNotInvestigator() {
- // Arrange
- Ticket ticket = openTicket();
- User admin = adminUser();
- User reporter = reporterUser();
-
- when(securityUtils.getCurrentUser()).thenReturn(admin);
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
- when(userRepository.findById(reporter.getId())).thenReturn(Optional.of(reporter));
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketService.assignInvestigator(1L, reporter.getId()));
-
- // Assert
- assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode());
- verify(ticketRepository, never()).save(any());
- }
-
- }
-
- @Nested
- @DisplayName("unassignInvestigator tests")
- class UnassignInvestigatorTests {
-
- @Test
- @DisplayName("Unassigning investigator should succeed")
- void unassignInvestigator_shouldSucceed() {
- // Arrange
- Ticket ticket = openTicket();
- User admin = adminUser();
- User investigator = investigatorUser();
- investigator.setUsername("inv-user");
- ticket.setInvestigator(investigator);
- ticket.setStatus(TicketStatus.IN_PROGRESS);
-
- when(securityUtils.getCurrentUser()).thenReturn(admin);
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
- when(ticketRepository.save(any())).thenReturn(ticket);
- when(ticketMapper.entityToViewDTO(any())).thenReturn(new TicketViewDTO());
-
- // Act
- ticketService.unassignInvestigator(1L);
-
- // Assert
- assertNull(ticket.getInvestigator());
- assertEquals(TicketStatus.OPEN, ticket.getStatus());
- verify(ticketRepository).save(ticket);
-
- verify(auditService).log(
- eq(AuditAction.UNASSIGNED),
- eq("investigator"),
- eq("inv-user"),
- isNull(),
- any(),
- eq(admin)
- );
- }
-
- @Test
- @DisplayName("Unassigning investigator should throw Bad Request when no investigator is assigned")
- void unassignInvestigator_shouldThrowBadRequest_whenNoInvestigatorAssigned() {
- // Arrange
- Ticket ticket = openTicket();
- User admin = adminUser();
- ticket.setInvestigator(null);
-
- when(securityUtils.getCurrentUser()).thenReturn(admin);
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketService.unassignInvestigator(1L));
-
- // Assert
- assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode());
- verify(ticketRepository, never()).save(any());
- }
-
- @Test
- @DisplayName("Unassigning investigator should throw Bad Request when ticket is not in progress")
- void unassignInvestigator_shouldThrowBadRequest_whenTicketNotInProgress() {
- // Arrange
- Ticket ticket = openTicket();
- User admin = adminUser();
- User investigator = investigatorUser();
- ticket.setInvestigator(investigator);
- ticket.setStatus(TicketStatus.OPEN);
-
- when(securityUtils.getCurrentUser()).thenReturn(admin);
- when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
-
- // Act
- ResponseStatusException ex = assertThrows(ResponseStatusException.class, () ->
- ticketService.unassignInvestigator(1L));
-
- // Assert
- assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode());
- verify(ticketRepository, never()).save(any());
- }
- }
-}
\ No newline at end of file
+ @Mock TicketRepository ticketRepository;
+ @Mock TicketMapper ticketMapper;
+ @Mock UserRepository userRepository;
+ @Mock SecurityUtils securityUtils;
+ @Mock AuditService auditService;
+
+ @InjectMocks TicketService ticketService;
+
+ private Ticket openTicket() {
+ Ticket t = new Ticket();
+ t.setId(1L);
+ t.setStatus(TicketStatus.OPEN);
+ return t;
+ }
+
+ private User adminUser() {
+ User u = new User();
+ u.setId(100L);
+ u.setRole(Role.ADMIN);
+ return u;
+ }
+
+ private User investigatorUser() {
+ User u = new User();
+ u.setId(200L);
+ u.setRole(Role.INVESTIGATOR);
+ return u;
+ }
+
+ private User reporterUser() {
+ User u = new User();
+ u.setId(300L);
+ u.setRole(Role.REPORTER);
+ return u;
+ }
+
+ @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
+ @DisplayName("getTicketByToken tests")
+ class GetTicketByTokenTests {
+
+ @Test
+ @DisplayName("Valid token should return ticket")
+ void getTicketByToken_shouldReturnTicket() {
+ // Arrange
+ Ticket ticket = new Ticket();
+ String token = "valid-token";
+
+ when(ticketRepository.findByReporterToken(token)).thenReturn(Optional.of(ticket));
+ when(ticketMapper.entityToViewDTO(any())).thenReturn(new TicketViewDTO());
+
+ // Act + Assert
+ assertDoesNotThrow(() -> ticketService.getTicketByToken(token));
+ verify(ticketMapper).entityToViewDTO(ticket);
+ }
+
+ @Test
+ @DisplayName("Invalid token should throw Not found")
+ void getTicketByToken_invalidToken_shouldThrowNotFound() {
+ // Arrange
+ when(ticketRepository.findByReporterToken(any())).thenReturn(Optional.empty());
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class, () -> ticketService.getTicketByToken("invalid-token"));
+
+ // Assert
+ assertEquals(HttpStatus.NOT_FOUND, ex.getStatusCode());
+ }
+ }
+
+ @Nested
+ @DisplayName("getTicketById tests")
+ class GetTicketByIdTests {
+
+ @Test
+ @DisplayName("getTicketById should return ticket when found and access is granted")
+ void getTicketById_shouldReturnTicket_whenFound() {
+ // Arrange
+ Ticket ticket = openTicket();
+ User admin = adminUser();
+ TicketViewDTO expected = new TicketViewDTO();
+
+ when(securityUtils.getCurrentUser()).thenReturn(admin);
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+ when(ticketMapper.entityToViewDTO(ticket)).thenReturn(expected);
+
+ // Act
+ TicketViewDTO result = ticketService.getTicketById(1L);
+
+ // Assert
+ assertSame(expected, result);
+ verify(ticketMapper).entityToViewDTO(ticket);
+ }
+
+ @Test
+ @DisplayName("getTicketById should throw Not Found when ticket does not exist")
+ void getTicketById_shouldThrow_whenNotFound() {
+ // Arrange
+ when(ticketRepository.findById(1L)).thenReturn(Optional.empty());
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(ResponseStatusException.class, () -> ticketService.getTicketById(1L));
+
+ // Assert
+ assertEquals(HttpStatus.NOT_FOUND, ex.getStatusCode());
+ }
+ }
+
+ @Nested
+ @DisplayName("getMyTickets tests")
+ class GetMyTicketsTests {
+
+ @Test
+ @DisplayName("Should return tickets for the current reporter")
+ void getMyTickets_shouldReturnTicketsForCurrentUser() {
+ // Arrange
+ User reporter = reporterUser();
+ Ticket ticket = openTicket();
+ TicketViewDTO expected = new TicketViewDTO();
+
+ when(securityUtils.getCurrentUser()).thenReturn(reporter);
+ when(ticketRepository.findByReporterId(reporter.getId())).thenReturn(List.of(ticket));
+ when(ticketMapper.entityToViewDTO(ticket)).thenReturn(expected);
+
+ // Act
+ List result = ticketService.getMyTickets();
+
+ // Assert
+ assertEquals(1, result.size());
+ assertSame(expected, result.getFirst());
+ verify(ticketRepository).findByReporterId(reporter.getId());
+ }
+
+ @Test
+ @DisplayName("Should return empty list when reporter has no tickets")
+ void getMyTickets_shouldReturnEmptyList_whenNoTickets() {
+ // Arrange
+ User reporter = reporterUser();
+
+ when(securityUtils.getCurrentUser()).thenReturn(reporter);
+ when(ticketRepository.findByReporterId(reporter.getId())).thenReturn(List.of());
+
+ // Act
+ List result = ticketService.getMyTickets();
+
+ // Assert
+ assertTrue(result.isEmpty());
+ verify(ticketMapper, never()).entityToViewDTO(any());
+ }
+ }
+
+ @Nested
+ @DisplayName("getMyAssignedTickets tests")
+ class GetMyAssignedTickets {
+
+ @Test
+ @DisplayName("Should return tickets for the current investigator")
+ void getMyAssignedTickets_shouldReturnTicketsForCurrentUser() {
+ // Arrange
+ User investigator = investigatorUser();
+ Ticket ticket = openTicket();
+ TicketViewDTO expected = new TicketViewDTO();
+
+ when(securityUtils.getCurrentUser()).thenReturn(investigator);
+ when(ticketRepository.findByInvestigatorId(investigator.getId())).thenReturn(List.of(ticket));
+ when(ticketMapper.entityToViewDTO(ticket)).thenReturn(expected);
+
+ // Act
+ List result = ticketService.getMyAssignedTickets();
+
+ // Assert
+ assertEquals(1, result.size());
+ assertSame(expected, result.getFirst());
+ verify(ticketRepository).findByInvestigatorId(investigator.getId());
+ }
+
+ @Test
+ @DisplayName("Should return empty list when investigator has no tickets")
+ void getMyAssignedTickets_shouldReturnEmptyList_whenNoTickets() {
+ // Arrange
+ User investigator = investigatorUser();
+
+ when(securityUtils.getCurrentUser()).thenReturn(investigator);
+ when(ticketRepository.findByInvestigatorId(investigator.getId())).thenReturn(List.of());
+
+ // Act
+ List result = ticketService.getMyAssignedTickets();
+
+ // Assert
+ assertTrue(result.isEmpty());
+ verify(ticketMapper, never()).entityToViewDTO(any());
+ }
+ }
+
+ @Nested
+ @DisplayName("getTicketsByStatus tests")
+ class GetTicketsByStatusTests {
+
+ @Test
+ @DisplayName("Admin can get tickets by status")
+ void admin_canGetTicketsByStatus() {
+ // Arrange
+ when(securityUtils.getCurrentUser()).thenReturn(adminUser());
+ when(ticketRepository.findByStatus(TicketStatus.OPEN)).thenReturn(List.of());
+
+ // Act + Assert
+ assertDoesNotThrow(() -> ticketService.getTicketsByStatus(TicketStatus.OPEN));
+ }
+
+ @Test
+ @DisplayName("Non-admin cannot get tickets by status")
+ void nonAdmin_cannotGetTicketsByStatus() {
+ // Arrange
+ when(securityUtils.getCurrentUser()).thenReturn(investigatorUser());
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () -> ticketService.getTicketsByStatus(TicketStatus.OPEN));
+
+ // Assert
+ assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
+ }
+ }
+
+ @Nested
+ @DisplayName("getTicketsByStatusAndInvestigator tests")
+ class GetTicketsByStatusAndInvestigatorTests {
+
+ @Test
+ @DisplayName("Admin can filter any investigator's tickets")
+ void admin_canFilterAnyInvestigator() {
+ // Arrange
+ User admin = adminUser();
+
+ when(securityUtils.getCurrentUser()).thenReturn(admin);
+ when(ticketRepository.findByStatusAndInvestigatorId(any(), any())).thenReturn(List.of());
+
+ // Act + Assert
+ assertDoesNotThrow(
+ () -> ticketService.getTicketsByStatusAndInvestigator(TicketStatus.OPEN, 200L));
+ }
+
+ @Test
+ @DisplayName("Investigator can filter their own tickets")
+ void investigator_canFilterOwnTickets() {
+ // Arrange
+ User investigator = investigatorUser();
+
+ when(securityUtils.getCurrentUser()).thenReturn(investigator);
+ when(ticketRepository.findByStatusAndInvestigatorId(any(), any())).thenReturn(List.of());
+
+ // Act + Assert
+ assertDoesNotThrow(
+ () -> ticketService.getTicketsByStatusAndInvestigator(TicketStatus.IN_PROGRESS, 200L));
+ }
+
+ @Test
+ @DisplayName("Investigator cannot filter other investigator's tickets")
+ void investigator_cannotFilterOthersTickets() {
+ // Arrange
+ User investigator = investigatorUser();
+
+ when(securityUtils.getCurrentUser()).thenReturn(investigator);
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () ->
+ ticketService.getTicketsByStatusAndInvestigator(TicketStatus.IN_PROGRESS, 201L));
+
+ // Assert
+ assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
+ }
+
+ @Test
+ @DisplayName("Reporter should be forbidden from filtering by investigator ID")
+ void reporter_cannotFilterTickets_byInvestigatorId() {
+ // Arrange
+ User reporter = reporterUser();
+
+ when(securityUtils.getCurrentUser()).thenReturn(reporter);
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () ->
+ ticketService.getTicketsByStatusAndInvestigator(TicketStatus.IN_PROGRESS, 200L));
+
+ // Assert
+ assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
+ }
+ }
+
+ @Nested
+ @DisplayName("checkAccess tests")
+ class CheckAccessTests {
+
+ @Test
+ @DisplayName("Admin should always have access")
+ void admin_shouldHaveAccess() {
+ // Arrange
+ Ticket ticket = openTicket();
+ User admin = adminUser();
+
+ when(securityUtils.getCurrentUser()).thenReturn(admin);
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+ when(ticketMapper.entityToViewDTO(any())).thenReturn(new TicketViewDTO());
+
+ // Act + Assert
+ assertDoesNotThrow(() -> ticketService.getTicketById(1L));
+ }
+
+ @Test
+ @DisplayName("Assigned investigator should have access")
+ void investigator_shouldHaveAccessIfAssigned() {
+ // Arrange
+ Ticket ticket = openTicket();
+ User investigator = investigatorUser();
+ ticket.setInvestigator(investigator);
+
+ when(securityUtils.getCurrentUser()).thenReturn(investigator);
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+ when(ticketMapper.entityToViewDTO(any())).thenReturn(new TicketViewDTO());
+
+ // Act + Assert
+ assertDoesNotThrow(() -> ticketService.getTicketById(1L));
+ }
+
+ @Test
+ @DisplayName("Unassigned investigator should not have access")
+ void unassignedInvestigator_shouldThrowForbidden() {
+ // Arrange
+ Ticket ticket = openTicket();
+ User investigator = investigatorUser();
+
+ User otherInvestigator = new User();
+ otherInvestigator.setId(201L);
+ otherInvestigator.setRole(Role.INVESTIGATOR);
+ ticket.setInvestigator(otherInvestigator);
+
+ when(securityUtils.getCurrentUser()).thenReturn(investigator);
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(ResponseStatusException.class, () -> ticketService.getTicketById(1L));
+
+ // Assert
+ assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
+ }
+
+ @Test
+ @DisplayName("Reporter who owns the ticket should have access")
+ void owningReporter_shouldHaveAccess() {
+ // Arrange
+ Ticket ticket = openTicket();
+ User reporter = reporterUser();
+ ticket.setReporter(reporter);
+
+ when(securityUtils.getCurrentUser()).thenReturn(reporter);
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+ when(ticketMapper.entityToViewDTO(any())).thenReturn(new TicketViewDTO());
+
+ // Act + Assert
+ assertDoesNotThrow(() -> ticketService.getTicketById(1L));
+ }
+
+ @Test
+ @DisplayName("Reporter who does not own the ticket should not have access")
+ void nonOwningReporter_shouldThrowForbidden() {
+ // Arrange
+ Ticket ticket = openTicket();
+ User reporter = reporterUser();
+
+ User otherReporter = new User();
+ otherReporter.setId(301L);
+ otherReporter.setRole(Role.REPORTER);
+ ticket.setReporter(otherReporter);
+
+ when(securityUtils.getCurrentUser()).thenReturn(reporter);
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(ResponseStatusException.class, () -> ticketService.getTicketById(1L));
+
+ // Assert
+ assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
+ }
+
+ @Test
+ @DisplayName("Investigator with null investigator set on ticket should be forbidden")
+ void investigator_nullInvestigatorOnTicket_shouldThrowForbidden() {
+ Ticket ticket = openTicket();
+ User investigator = investigatorUser();
+ ticket.setInvestigator(null);
+
+ when(securityUtils.getCurrentUser()).thenReturn(investigator);
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+
+ ResponseStatusException ex =
+ assertThrows(ResponseStatusException.class, () -> ticketService.getTicketById(1L));
+
+ assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
+ }
+ }
+
+ @Nested
+ @DisplayName("requireCurrentUser tests")
+ class RequireCurrentUserTests {
+
+ @Test
+ @DisplayName("No authenticated user in security context should throw Unauthorized")
+ void noAuthenticatedUser_shouldThrowUnauthorized() {
+ // Arrange
+ when(securityUtils.getCurrentUser())
+ .thenThrow(new RuntimeException("No authenticated user in security context"));
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(ResponseStatusException.class, () -> ticketService.getMyTickets());
+
+ // Assert
+ assertEquals(HttpStatus.UNAUTHORIZED, ex.getStatusCode());
+ verify(ticketRepository, never()).findByReporterId(any());
+ }
+
+ @Test
+ @DisplayName("Authenticated user not found in database should throw Unauthorized")
+ void userNotFoundInDatabase_shouldThrowUnauthorized() {
+ // Arrange
+ when(securityUtils.getCurrentUser())
+ .thenThrow(new RuntimeException("Authenticated user not found in database"));
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(ResponseStatusException.class, () -> ticketService.getMyTickets());
+
+ // Assert
+ assertEquals(HttpStatus.UNAUTHORIZED, ex.getStatusCode());
+ verify(ticketRepository, never()).findByReporterId(any());
+ }
+ }
+
+ @Nested
+ @DisplayName("updateTicketStatus tests")
+ class UpdateTicketStatusTests {
+
+ @Test
+ @DisplayName("Valid transition should succeed")
+ void validTransition_shouldSucceed() {
+ // Arrange
+ Ticket ticket = openTicket();
+ User investigator = investigatorUser();
+ ticket.setInvestigator(investigator);
+
+ when(securityUtils.getCurrentUser()).thenReturn(investigator);
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+ when(ticketRepository.save(any())).thenReturn(ticket);
+ when(ticketMapper.entityToViewDTO(any())).thenReturn(new TicketViewDTO());
+
+ // Act
+ ticketService.updateTicketStatus(1L, TicketStatus.IN_PROGRESS);
+
+ // Assert
+ assertEquals(TicketStatus.IN_PROGRESS, ticket.getStatus());
+ verify(ticketRepository).save(ticket);
+ }
+
+ @Test
+ @DisplayName("Invalid transition should throw Bad Request")
+ void invalidTransition_shouldThrowBadRequest() {
+ // Arrange
+ Ticket ticket = openTicket();
+ User admin = adminUser();
+
+ when(securityUtils.getCurrentUser()).thenReturn(admin);
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () -> ticketService.updateTicketStatus(1L, TicketStatus.CLOSED));
+
+ // Assert
+ assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode());
+ verify(ticketRepository, never()).save(any());
+ }
+
+ @Test
+ @DisplayName("Reporter should not be able to update status")
+ void reporterStatusUpdate_shouldThrowForbidden() {
+ // Arrange
+ Ticket ticket = openTicket();
+ User reporter = reporterUser();
+
+ when(securityUtils.getCurrentUser()).thenReturn(reporter);
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () -> ticketService.updateTicketStatus(1L, TicketStatus.CLOSED));
+
+ // Assert
+ assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
+ verify(ticketRepository, never()).save(any());
+ }
+
+ @Test
+ @DisplayName("Same status should return without changes")
+ void sameStatus_shouldReturn() {
+ // Arrange
+ Ticket ticket = openTicket();
+ User admin = adminUser();
+
+ when(securityUtils.getCurrentUser()).thenReturn(admin);
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+ when(ticketMapper.entityToViewDTO(any())).thenReturn(new TicketViewDTO());
+
+ // Act
+ TicketViewDTO result = ticketService.updateTicketStatus(1L, TicketStatus.OPEN);
+
+ // Assert
+ assertNotNull(result);
+ verify(ticketRepository, never()).save(any());
+ }
+
+ @Test
+ @DisplayName("Status change without investigator should throw Bad Request")
+ void statusChange_withoutInvestigator_shouldThrowBadRequest() {
+ // Arrange
+ Ticket ticket = openTicket();
+ User admin = adminUser();
+ ticket.setInvestigator(null);
+
+ when(securityUtils.getCurrentUser()).thenReturn(admin);
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () -> ticketService.updateTicketStatus(1L, TicketStatus.IN_PROGRESS));
+
+ // Assert
+ assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode());
+ verify(ticketRepository, never()).save(any());
+ }
+
+ @Test
+ @DisplayName("Investigator not assigned should be forbidden")
+ void investigator_notAssigned_shouldBeForbidden() {
+ // Arrange
+ Ticket ticket = openTicket();
+ User investigator = investigatorUser();
+
+ User otherInvestigator = new User();
+ otherInvestigator.setId(201L);
+ otherInvestigator.setRole(Role.INVESTIGATOR);
+ ticket.setInvestigator(otherInvestigator);
+
+ when(securityUtils.getCurrentUser()).thenReturn(investigator);
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () -> ticketService.updateTicketStatus(1L, TicketStatus.IN_PROGRESS));
+
+ // Assert
+ assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode());
+ verify(ticketRepository, never()).save(any());
+ }
+ }
+
+ @Nested
+ @DisplayName("assignInvestigator tests")
+ class AssignInvestigatorTests {
+
+ @Test
+ @DisplayName("Assigning investigator should succeed")
+ void assignInvestigator_shouldSucceed() {
+ // Arrange
+ Ticket ticket = openTicket();
+ User admin = adminUser();
+ User investigator = investigatorUser();
+
+ when(securityUtils.getCurrentUser()).thenReturn(admin);
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+ when(userRepository.findById(investigator.getId())).thenReturn(Optional.of(investigator));
+ when(ticketRepository.save(any())).thenReturn(ticket);
+ when(ticketMapper.entityToViewDTO(any())).thenReturn(new TicketViewDTO());
+
+ // Act
+ ticketService.assignInvestigator(1L, investigator.getId());
+
+ // Assert
+ assertEquals(investigator, ticket.getInvestigator());
+ assertEquals(TicketStatus.IN_PROGRESS, ticket.getStatus());
+ verify(ticketRepository).save(ticket);
+ }
+
+ @Test
+ @DisplayName("Assigning investigator should throw Bad Request when investigatorId is null")
+ void assignInvestigator_shouldThrowBadRequest_whenInvestigatorIsNull() {
+ // Arrange + Act
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class, () -> ticketService.assignInvestigator(1L, null));
+
+ // Assert
+ assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode());
+ verify(ticketRepository, never()).save(any());
+ }
+
+ @Test
+ @DisplayName("Assigning investigator should throw Conflict when ticket is already assigned")
+ void assignInvestigator_shouldThrowConflict_whenTicketAlreadyAssigned() {
+ // Arrange
+ Ticket ticket = openTicket();
+ User admin = adminUser();
+ User investigator = investigatorUser();
+ ticket.setInvestigator(investigator);
+
+ when(securityUtils.getCurrentUser()).thenReturn(admin);
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () -> ticketService.assignInvestigator(1L, investigator.getId()));
+
+ // Assert
+ assertEquals(HttpStatus.CONFLICT, ex.getStatusCode());
+ verify(ticketRepository, never()).save(any());
+ }
+
+ @Test
+ @DisplayName("Assigning investigator should throw Bad Request when ticket is not open")
+ void assignInvestigator_shouldThrowBadRequest_whenIsNotOpen() {
+ // Arrange
+ Ticket ticket = openTicket();
+ User admin = adminUser();
+ User investigator = investigatorUser();
+ ticket.setStatus(TicketStatus.CLOSED);
+
+ when(securityUtils.getCurrentUser()).thenReturn(admin);
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () -> ticketService.assignInvestigator(1L, investigator.getId()));
+
+ // Assert
+ assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode());
+ verify(ticketRepository, never()).save(any());
+ }
+
+ @Test
+ @DisplayName("Assigning investigator should throw Bad Request when user is not investigator")
+ void assignInvestigator_shouldThrowBadRequest_whenUserIsNotInvestigator() {
+ // Arrange
+ Ticket ticket = openTicket();
+ User admin = adminUser();
+ User reporter = reporterUser();
+
+ when(securityUtils.getCurrentUser()).thenReturn(admin);
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+ when(userRepository.findById(reporter.getId())).thenReturn(Optional.of(reporter));
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(
+ ResponseStatusException.class,
+ () -> ticketService.assignInvestigator(1L, reporter.getId()));
+
+ // Assert
+ assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode());
+ verify(ticketRepository, never()).save(any());
+ }
+ }
+
+ @Nested
+ @DisplayName("unassignInvestigator tests")
+ class UnassignInvestigatorTests {
+
+ @Test
+ @DisplayName("Unassigning investigator should succeed")
+ void unassignInvestigator_shouldSucceed() {
+ // Arrange
+ Ticket ticket = openTicket();
+ User admin = adminUser();
+ User investigator = investigatorUser();
+ investigator.setUsername("inv-user");
+ ticket.setInvestigator(investigator);
+ ticket.setStatus(TicketStatus.IN_PROGRESS);
+
+ when(securityUtils.getCurrentUser()).thenReturn(admin);
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+ when(ticketRepository.save(any())).thenReturn(ticket);
+ when(ticketMapper.entityToViewDTO(any())).thenReturn(new TicketViewDTO());
+
+ // Act
+ ticketService.unassignInvestigator(1L);
+
+ // Assert
+ assertNull(ticket.getInvestigator());
+ assertEquals(TicketStatus.OPEN, ticket.getStatus());
+ verify(ticketRepository).save(ticket);
+
+ verify(auditService)
+ .log(
+ eq(AuditAction.UNASSIGNED),
+ eq("investigator"),
+ eq("inv-user"),
+ isNull(),
+ any(),
+ eq(admin));
+ }
+
+ @Test
+ @DisplayName(
+ "Unassigning investigator should throw Bad Request when no investigator is assigned")
+ void unassignInvestigator_shouldThrowBadRequest_whenNoInvestigatorAssigned() {
+ // Arrange
+ Ticket ticket = openTicket();
+ User admin = adminUser();
+ ticket.setInvestigator(null);
+
+ when(securityUtils.getCurrentUser()).thenReturn(admin);
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(ResponseStatusException.class, () -> ticketService.unassignInvestigator(1L));
+
+ // Assert
+ assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode());
+ verify(ticketRepository, never()).save(any());
+ }
+
+ @Test
+ @DisplayName("Unassigning investigator should throw Bad Request when ticket is not in progress")
+ void unassignInvestigator_shouldThrowBadRequest_whenTicketNotInProgress() {
+ // Arrange
+ Ticket ticket = openTicket();
+ User admin = adminUser();
+ User investigator = investigatorUser();
+ ticket.setInvestigator(investigator);
+ ticket.setStatus(TicketStatus.OPEN);
+
+ when(securityUtils.getCurrentUser()).thenReturn(admin);
+ when(ticketRepository.findById(1L)).thenReturn(Optional.of(ticket));
+
+ // Act
+ ResponseStatusException ex =
+ assertThrows(ResponseStatusException.class, () -> ticketService.unassignInvestigator(1L));
+
+ // Assert
+ assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode());
+ verify(ticketRepository, never()).save(any());
+ }
+ }
+}
diff --git a/src/test/java/org/example/alfs/services/UserServiceTest.java b/src/test/java/org/example/alfs/services/UserServiceTest.java
index 10947f9..ff0c6e6 100644
--- a/src/test/java/org/example/alfs/services/UserServiceTest.java
+++ b/src/test/java/org/example/alfs/services/UserServiceTest.java
@@ -1,5 +1,10 @@
package org.example.alfs.services;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
import org.example.alfs.entities.User;
import org.example.alfs.enums.Role;
import org.example.alfs.repositories.UserRepository;
@@ -10,49 +15,40 @@
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
-import java.util.List;
+@ExtendWith(MockitoExtension.class)
+class UserServiceTest {
-import static org.assertj.core.api.Assertions.assertThat;
+ @Mock private UserRepository userRepository;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
+ @InjectMocks private UserService userService;
-@ExtendWith(MockitoExtension.class)
-class UserServiceTest {
+ @Test
+ @DisplayName("getAllInvestigators should return all investigators from the repository")
+ void getAllInvestigators_returnsInvestigatorsFromRepository() {
+ // Arrange
+ User investigator = new User();
+ investigator.setRole(Role.INVESTIGATOR);
+ when(userRepository.findByRole(Role.INVESTIGATOR)).thenReturn(List.of(investigator));
+
+ // Act
+ List result = userService.getAllInvestigators();
+
+ // Assert
+ assertThat(result).containsExactly(investigator);
+ verify(userRepository).findByRole(Role.INVESTIGATOR);
+ }
+
+ @Test
+ @DisplayName("getAllInvestigators should return an empty list when no investigators exist")
+ void getAllInvestigators_whenNoneExist_returnsEmptyList() {
+ // Arrange
+ when(userRepository.findByRole(Role.INVESTIGATOR)).thenReturn(List.of());
+
+ // Act
+ List result = userService.getAllInvestigators();
- @Mock
- private UserRepository userRepository;
-
- @InjectMocks
- private UserService userService;
-
- @Test
- @DisplayName("getAllInvestigators should return all investigators from the repository")
- void getAllInvestigators_returnsInvestigatorsFromRepository() {
- // Arrange
- User investigator = new User();
- investigator.setRole(Role.INVESTIGATOR);
- when(userRepository.findByRole(Role.INVESTIGATOR)).thenReturn(List.of(investigator));
-
- // Act
- List result = userService.getAllInvestigators();
-
- // Assert
- assertThat(result).containsExactly(investigator);
- verify(userRepository).findByRole(Role.INVESTIGATOR);
- }
-
- @Test
- @DisplayName("getAllInvestigators should return an empty list when no investigators exist")
- void getAllInvestigators_whenNoneExist_returnsEmptyList() {
- // Arrange
- when(userRepository.findByRole(Role.INVESTIGATOR)).thenReturn(List.of());
-
- // Act
- List result = userService.getAllInvestigators();
-
- // Assert
- assertThat(result).isEmpty();
- verify(userRepository).findByRole(Role.INVESTIGATOR);
- }
-}
\ No newline at end of file
+ // Assert
+ assertThat(result).isEmpty();
+ verify(userRepository).findByRole(Role.INVESTIGATOR);
+ }
+}