From fb81d88894fdae7a3efc94661682b8e6bcadcfed Mon Sep 17 00:00:00 2001 From: zkhr Date: Mon, 15 Sep 2025 12:40:24 +0200 Subject: [PATCH 01/11] added update method --- .../dto/UpdateLinkExpirationRequestDto.java | 24 +++++++++ .../InvalidExpirationDateException.java | 19 +++++++ .../api/security/config/SecurityConfig.java | 1 + .../api/service/LinkService.java | 26 ++++++++++ .../api/v1/controller/LinkCrudController.java | 34 ++++++++---- .../controller/LinkCrudControllerTest.java | 31 +++++++++++ .../api/service/LinkServiceTest.java | 52 +++++++++++++++---- 7 files changed, 167 insertions(+), 20 deletions(-) create mode 100644 src/main/java/org/decepticons/linkshortener/api/dto/UpdateLinkExpirationRequestDto.java create mode 100644 src/main/java/org/decepticons/linkshortener/api/exception/InvalidExpirationDateException.java diff --git a/src/main/java/org/decepticons/linkshortener/api/dto/UpdateLinkExpirationRequestDto.java b/src/main/java/org/decepticons/linkshortener/api/dto/UpdateLinkExpirationRequestDto.java new file mode 100644 index 0000000..d67cb51 --- /dev/null +++ b/src/main/java/org/decepticons/linkshortener/api/dto/UpdateLinkExpirationRequestDto.java @@ -0,0 +1,24 @@ +package org.decepticons.linkshortener.api.dto; + +import java.time.Instant; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; + + +/** + * Data Transfer Object (DTO) for updating the expiration date of a shortened link. + * This object is used to encapsulate the data required to update the expiration + * date of an existing short link in the system. + */ +@Getter +@Setter +@AllArgsConstructor +public class UpdateLinkExpirationRequestDto { + + @NonNull + private Instant newExpirationDate; + + +} diff --git a/src/main/java/org/decepticons/linkshortener/api/exception/InvalidExpirationDateException.java b/src/main/java/org/decepticons/linkshortener/api/exception/InvalidExpirationDateException.java new file mode 100644 index 0000000..bfca945 --- /dev/null +++ b/src/main/java/org/decepticons/linkshortener/api/exception/InvalidExpirationDateException.java @@ -0,0 +1,19 @@ +package org.decepticons.linkshortener.api.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.time.Instant; + +@ResponseStatus(HttpStatus.BAD_REQUEST) +@Getter +public class InvalidExpirationDateException extends RuntimeException { + private final Instant invalidDate; + + public InvalidExpirationDateException(String message, Instant invalidDate) { + super(message); + this.invalidDate = invalidDate; + } + +} diff --git a/src/main/java/org/decepticons/linkshortener/api/security/config/SecurityConfig.java b/src/main/java/org/decepticons/linkshortener/api/security/config/SecurityConfig.java index b2db1c1..def7356 100644 --- a/src/main/java/org/decepticons/linkshortener/api/security/config/SecurityConfig.java +++ b/src/main/java/org/decepticons/linkshortener/api/security/config/SecurityConfig.java @@ -61,6 +61,7 @@ public SecurityFilterChain securityFilterChain( "/api/links/**", // Documentation & health endpoints + "/api/v1/cache", "/health", "/h2-console/**", "/swagger-ui.html", diff --git a/src/main/java/org/decepticons/linkshortener/api/service/LinkService.java b/src/main/java/org/decepticons/linkshortener/api/service/LinkService.java index ca97fc4..8fd4e96 100644 --- a/src/main/java/org/decepticons/linkshortener/api/service/LinkService.java +++ b/src/main/java/org/decepticons/linkshortener/api/service/LinkService.java @@ -7,6 +7,7 @@ import org.decepticons.linkshortener.api.dto.LinkResponseDto; import org.decepticons.linkshortener.api.dto.UrlRequestDto; +import org.decepticons.linkshortener.api.exception.InvalidExpirationDateException; import org.decepticons.linkshortener.api.exception.NoSuchShortLinkFoundInTheSystemException; import org.decepticons.linkshortener.api.exception.NoSuchUserFoundInTheSystemException; import org.decepticons.linkshortener.api.model.Link; @@ -268,5 +269,30 @@ public User getCurrentUser() { username )); } + + @CachePut(value = "shortLinksCache", key = "#code") + public LinkResponseDto updateLinkExpiration(String code, Instant newExpirationDate) { + Link link = linkRepository.findByCode(code) + .orElseThrow(() -> new NoSuchShortLinkFoundInTheSystemException( + "No such short link found in the system: " + code, + code + )); + + if (!link.getOwner().getId().equals(getCurrentUserId())) { + throw new AccessDeniedException("You are not allowed to update this link"); + } + + if (newExpirationDate.isBefore(Instant.now())) { + throw new InvalidExpirationDateException( + "Expiration date must be in the future", + newExpirationDate + ); + } + + link.setExpiresAt(newExpirationDate); + linkRepository.save(link); + + return mapToResponse(link); + } } diff --git a/src/main/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudController.java b/src/main/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudController.java index f59240a..35658d6 100644 --- a/src/main/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudController.java +++ b/src/main/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudController.java @@ -1,26 +1,20 @@ package org.decepticons.linkshortener.api.v1.controller; import jakarta.validation.Valid; + import java.util.UUID; import org.decepticons.linkshortener.api.dto.LinkResponseDto; +import org.decepticons.linkshortener.api.dto.UpdateLinkExpirationRequestDto; import org.decepticons.linkshortener.api.dto.UrlRequestDto; import org.decepticons.linkshortener.api.model.User; import org.decepticons.linkshortener.api.service.LinkService; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; /** * REST controller for managing short links. - * Provides endpoints for creating, retrieving, and deleting short links. * All operations are performed in the context of the currently authenticated user. */ @@ -34,8 +28,7 @@ public class LinkCrudController { /** * Constructs a new {@link LinkCrudController} with the given dependencies. * - * @param linkService the service responsible for link business logic - + * @param linkService the service responsible for link business logic */ public LinkCrudController(LinkService linkService) { this.linkService = linkService; @@ -98,4 +91,23 @@ public ResponseEntity deleteLink(@PathVariable UUID id) { linkService.deleteLink(id); return ResponseEntity.noContent().build(); } + + + + /** + * Updates the expiration date of a specific link identified by its code. + * + * @param newExpirationDate DTO containing the new expiration date + * @param code the short URL code + * @return DTO with information about the updated link + */ + @PatchMapping("/{code}") + public ResponseEntity updateLinkExpiration( + @Valid @RequestBody UpdateLinkExpirationRequestDto newExpirationDate, + @PathVariable String code) { + + return ResponseEntity + .ok(linkService.updateLinkExpiration(code, newExpirationDate.getNewExpirationDate())); + } + } diff --git a/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java b/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java index 3989b21..a7c4f21 100644 --- a/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java @@ -2,6 +2,7 @@ import org.decepticons.linkshortener.api.dto.LinkResponseDto; +import org.decepticons.linkshortener.api.dto.UpdateLinkExpirationRequestDto; import org.decepticons.linkshortener.api.dto.UrlRequestDto; import org.decepticons.linkshortener.api.exception.NoSuchUserFoundInTheSystemException; import org.decepticons.linkshortener.api.model.User; @@ -228,4 +229,34 @@ void testDeleteLink_SUCCESS() { verify(linkService, times(1)).deleteLink(linkId); } + @Test + @DisplayName("Update Link Expiration Date - Success") + void testUpdateLinkExpirationDate(){ + + String code = "abc123"; + + UpdateLinkExpirationRequestDto requestDto + = new UpdateLinkExpirationRequestDto(Instant.now().plus(5, ChronoUnit.DAYS)); + + + when(linkService.updateLinkExpiration(code, requestDto.getNewExpirationDate())) + .thenReturn(new LinkResponseDto( + UUID.randomUUID(), + code, + "https://example.com/some/long/url", + Instant.now(), + requestDto.getNewExpirationDate(), + 0, + "ACTIVE", + UUID.randomUUID() + )); + + ResponseEntity response = linkController.updateLinkExpiration(requestDto, code); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + assertEquals(code, response.getBody().code()); + verify(linkService, times(1)) + .updateLinkExpiration(code, requestDto.getNewExpirationDate()); + } } diff --git a/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceTest.java b/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceTest.java index d285813..ab6e4e6 100644 --- a/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceTest.java @@ -15,6 +15,8 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; @@ -27,28 +29,25 @@ import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @ExtendWith(org.mockito.junit.jupiter.MockitoExtension.class) class LinkServiceTest { @Mock private LinkRepository linkRepository; - @InjectMocks - private LinkService linkService; + @Mock private UserRepository userRepository; + @InjectMocks + private LinkService linkService; + + - @BeforeEach - void setUp() { - linkRepository = mock(LinkRepository.class); - linkService = new LinkService(linkRepository, null, null); - } @Test @DisplayName("Link Creation - Success") @@ -203,4 +202,39 @@ void validateLink_Success() { Assertions.assertTrue(linkService.validateLink(linkResponseDto)); } + + @Test + @DisplayName("Update Link Expiration - Success") + void testUpdateLinkExpiration(){ + String code = "abc123"; + User owner = new User(); + owner.setUsername("testUser"); + owner.setId(UUID.randomUUID()); + + Link link = new Link(); + link.setCode(code); + link.setExpiresAt(Instant.now().plusSeconds(3600)); + link.setStatus(LinkStatus.ACTIVE); + link.setOwner(owner); + + Authentication auth = mock(Authentication.class); + SecurityContext securityContext = mock(SecurityContext.class); + when(securityContext.getAuthentication()).thenReturn(auth); + when(auth.getName()).thenReturn("testUser"); + SecurityContextHolder.setContext(securityContext); + + when(linkRepository.findByCode(code)).thenReturn(Optional.of(link)); + when(linkRepository.save(any(Link.class))).thenAnswer(i -> i.getArguments()[0]); + when(userRepository.findByUsername("testUser")).thenReturn(Optional.of(owner)); + + LinkResponseDto response = linkService.updateLinkExpiration(code, Instant + .now() + .plusSeconds(7200)); + + assertNotNull(response); + assertEquals(code, response.code()); + assertEquals(link.getExpiresAt(), response.expiresAt()); + verify(linkRepository, times(1)).findByCode(code); + + } } From 1e1094d5d9afeb9763c007a97d0acfa7ea4d2328 Mon Sep 17 00:00:00 2001 From: zkhr Date: Mon, 15 Sep 2025 12:47:16 +0200 Subject: [PATCH 02/11] fixed swagger for LinkCrudController --- .../api/v1/controller/LinkCrudController.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudController.java b/src/main/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudController.java index 35658d6..e1e55bf 100644 --- a/src/main/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudController.java +++ b/src/main/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudController.java @@ -1,5 +1,7 @@ package org.decepticons.linkshortener.api.v1.controller; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.util.UUID; @@ -18,6 +20,7 @@ * Provides endpoints for creating, retrieving, and deleting short links. * All operations are performed in the context of the currently authenticated user. */ +@Tag(name = "Link Management", description = "Operations for managing short links") @RestController @RequestMapping("/api/v1/links") public class LinkCrudController { @@ -42,6 +45,7 @@ public LinkCrudController(LinkService linkService) { * @return DTO with information about the created short URL */ @PostMapping + @Operation(summary = "Create a short URL for the current user") public ResponseEntity createLink(@Valid @RequestBody UrlRequestDto originalUrl) { User user = linkService.getCurrentUser(); @@ -58,6 +62,7 @@ public ResponseEntity createLink(@Valid @RequestBody UrlRequest * @return page of LinkResponseDto */ @GetMapping("/my_all_links") + @Operation(summary = "Get all links (active and inactive) for the current user") public ResponseEntity> getAllMyLinks( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size @@ -73,6 +78,7 @@ public ResponseEntity> getAllMyLinks( * @return page of LinkResponseDto */ @GetMapping("/my_all_active_links") + @Operation(summary = "Get all active links for the current user") public ResponseEntity> getAllMyActiveLinks( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size @@ -87,6 +93,7 @@ public ResponseEntity> getAllMyActiveLinks( * @return HTTP 204 No Content if deletion was successful */ @DeleteMapping("/delete/{id}") + @Operation(summary = "Delete a specific link of the current user by its ID") public ResponseEntity deleteLink(@PathVariable UUID id) { linkService.deleteLink(id); return ResponseEntity.noContent().build(); @@ -102,6 +109,7 @@ public ResponseEntity deleteLink(@PathVariable UUID id) { * @return DTO with information about the updated link */ @PatchMapping("/{code}") + @Operation(summary = "Update the expiration date of a specific link by its code") public ResponseEntity updateLinkExpiration( @Valid @RequestBody UpdateLinkExpirationRequestDto newExpirationDate, @PathVariable String code) { From d0d350955485ebb61b406845c1340da7b7ed5efa Mon Sep 17 00:00:00 2001 From: zkhr Date: Mon, 15 Sep 2025 15:39:26 +0200 Subject: [PATCH 03/11] improved exception handling and reduced dto classes --- .../GlobalExceptionHandlerController.java} | 72 ++++++++++++++++--- .../api/dto/ApiErrorResponseDto.java | 19 +++++ .../api/dto/NoSuchLinkFoundResponseDto.java | 26 ------- .../api/dto/NoSuchUserFoundResponseDto.java | 26 ------- .../dto/ShortLinkOutOfDateResponseDto.java | 21 ------ .../BaseException.java | 2 +- ...tomizedResponseEntityExceptionHandler.java | 70 ------------------ .../ExpiredTokenException.java | 4 +- .../InvalidPasswordException.java | 4 +- .../InvalidTokenException.java | 4 +- .../UserAlreadyExistsException.java | 4 +- .../UserNotFoundException.java | 4 +- .../package-info.java | 2 +- .../security/jwt/JwtAuthenticationFilter.java | 2 +- .../service/impl/AuthServiceImpl.java | 4 +- .../service/impl/UserAuthServiceImpl.java | 6 +- .../GlobalExceptionHandlerTest.java | 4 +- .../jwt/JwtAuthenticationFilterTest.java | 2 +- .../service/impl/AuthServiceImplTest.java | 8 +-- .../service/impl/UserAuthServiceImplTest.java | 6 +- 20 files changed, 114 insertions(+), 176 deletions(-) rename src/main/java/org/decepticons/linkshortener/api/{exceptions/GlobalExceptionHandler.java => controller/GlobalExceptionHandlerController.java} (61%) create mode 100644 src/main/java/org/decepticons/linkshortener/api/dto/ApiErrorResponseDto.java delete mode 100644 src/main/java/org/decepticons/linkshortener/api/dto/NoSuchLinkFoundResponseDto.java delete mode 100644 src/main/java/org/decepticons/linkshortener/api/dto/NoSuchUserFoundResponseDto.java delete mode 100644 src/main/java/org/decepticons/linkshortener/api/dto/ShortLinkOutOfDateResponseDto.java rename src/main/java/org/decepticons/linkshortener/api/{exceptions => exception}/BaseException.java (92%) delete mode 100644 src/main/java/org/decepticons/linkshortener/api/exception/CustomizedResponseEntityExceptionHandler.java rename src/main/java/org/decepticons/linkshortener/api/{exceptions => exception}/ExpiredTokenException.java (84%) rename src/main/java/org/decepticons/linkshortener/api/{exceptions => exception}/InvalidPasswordException.java (75%) rename src/main/java/org/decepticons/linkshortener/api/{exceptions => exception}/InvalidTokenException.java (84%) rename src/main/java/org/decepticons/linkshortener/api/{exceptions => exception}/UserAlreadyExistsException.java (78%) rename src/main/java/org/decepticons/linkshortener/api/{exceptions => exception}/UserNotFoundException.java (76%) rename src/main/java/org/decepticons/linkshortener/api/{exceptions => exception}/package-info.java (57%) diff --git a/src/main/java/org/decepticons/linkshortener/api/exceptions/GlobalExceptionHandler.java b/src/main/java/org/decepticons/linkshortener/api/controller/GlobalExceptionHandlerController.java similarity index 61% rename from src/main/java/org/decepticons/linkshortener/api/exceptions/GlobalExceptionHandler.java rename to src/main/java/org/decepticons/linkshortener/api/controller/GlobalExceptionHandlerController.java index f9a9391..f9519d5 100644 --- a/src/main/java/org/decepticons/linkshortener/api/exceptions/GlobalExceptionHandler.java +++ b/src/main/java/org/decepticons/linkshortener/api/controller/GlobalExceptionHandlerController.java @@ -1,19 +1,32 @@ -package org.decepticons.linkshortener.api.exceptions; +package org.decepticons.linkshortener.api.controller; import java.time.Instant; import java.util.Map; + +import org.decepticons.linkshortener.api.exception.*; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; -/** - * Centralized exception handler for REST controllers. - */ @ControllerAdvice -public class GlobalExceptionHandler { +public class GlobalExceptionHandlerController { private ResponseEntity> buildErrorResponse( + final HttpStatus status, + final String error, + final String message, + final Object details) { + return ResponseEntity.status(status).body(Map.of( + "timestamp", Instant.now().toString(), + "status", status.value(), + "error", error, + "message", message, + "details", details + )); + } + + private ResponseEntity> buildErrorResponseSecurity( final HttpStatus status, final String error, final String message) { @@ -25,6 +38,7 @@ private ResponseEntity> buildErrorResponse( )); } + /** * Handles exceptions when a user already exists. * @@ -34,7 +48,7 @@ private ResponseEntity> buildErrorResponse( @ExceptionHandler(UserAlreadyExistsException.class) public ResponseEntity> handleUserExists( final UserAlreadyExistsException ex) { - return buildErrorResponse( + return buildErrorResponseSecurity( HttpStatus.CONFLICT, "User Already Exists", ex.getMessage()); @@ -49,7 +63,7 @@ public ResponseEntity> handleUserExists( @ExceptionHandler(UserNotFoundException.class) public ResponseEntity> handleUserNotFound( final UserNotFoundException ex) { - return buildErrorResponse( + return buildErrorResponseSecurity( HttpStatus.NOT_FOUND, "User Not Found", ex.getMessage()); @@ -64,7 +78,7 @@ public ResponseEntity> handleUserNotFound( @ExceptionHandler(InvalidPasswordException.class) public ResponseEntity> handleInvalidPassword( final InvalidPasswordException ex) { - return buildErrorResponse( + return buildErrorResponseSecurity( HttpStatus.BAD_REQUEST, "Invalid Password", ex.getMessage()); @@ -79,7 +93,7 @@ public ResponseEntity> handleInvalidPassword( @ExceptionHandler(InvalidTokenException.class) public ResponseEntity> handleInvalidToken( final InvalidTokenException ex) { - return buildErrorResponse( + return buildErrorResponseSecurity( HttpStatus.UNAUTHORIZED, "Invalid Token", ex.getMessage()); @@ -94,7 +108,7 @@ public ResponseEntity> handleInvalidToken( @ExceptionHandler(ExpiredTokenException.class) public ResponseEntity> handleExpiredToken( final ExpiredTokenException ex) { - return buildErrorResponse(HttpStatus.UNAUTHORIZED, + return buildErrorResponseSecurity(HttpStatus.UNAUTHORIZED, "Expired Token", ex.getMessage()); } @@ -107,8 +121,44 @@ public ResponseEntity> handleExpiredToken( */ @ExceptionHandler(Exception.class) public ResponseEntity> handleGeneric(final Exception ex) { - return buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, + return buildErrorResponseSecurity(HttpStatus.INTERNAL_SERVER_ERROR, "Server Error", ex.getMessage()); } + + // === Link Shortener специфичные ошибки === + @ExceptionHandler(NoSuchUserFoundInTheSystemException.class) + public ResponseEntity> handleNoSuchUser(NoSuchUserFoundInTheSystemException ex) { + return buildErrorResponse( + HttpStatus.NOT_FOUND, + "User Not Found", + "No such user in the system", + Map.of("username", ex.getUsername()) + ); + } + + @ExceptionHandler(NoSuchShortLinkFoundInTheSystemException.class) + public ResponseEntity> handleNoSuchLink(NoSuchShortLinkFoundInTheSystemException ex) { + return buildErrorResponse( + HttpStatus.NOT_FOUND, + "Short Link Not Found", + "No such short link in the system", + Map.of("shortLink", ex.getShortLink()) + ); + } + + @ExceptionHandler(ShortLinkIsOutOfDateException.class) + public ResponseEntity> handleShortLinkOutOfDate(ShortLinkIsOutOfDateException ex) { + return buildErrorResponse( + HttpStatus.GONE, + "Short Link Expired", + "The short link is out of date", + Map.of( + "shortLink", ex.getShortLink(), + "expiredAt", ex.getExpiredAt() + ) + ); + } + + } diff --git a/src/main/java/org/decepticons/linkshortener/api/dto/ApiErrorResponseDto.java b/src/main/java/org/decepticons/linkshortener/api/dto/ApiErrorResponseDto.java new file mode 100644 index 0000000..7fd7a14 --- /dev/null +++ b/src/main/java/org/decepticons/linkshortener/api/dto/ApiErrorResponseDto.java @@ -0,0 +1,19 @@ +package org.decepticons.linkshortener.api.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@AllArgsConstructor +public class ApiErrorResponseDto { + private LocalDateTime timestamp; + private int status; + private String error; + private String message; + private String path; + private Object details; +} diff --git a/src/main/java/org/decepticons/linkshortener/api/dto/NoSuchLinkFoundResponseDto.java b/src/main/java/org/decepticons/linkshortener/api/dto/NoSuchLinkFoundResponseDto.java deleted file mode 100644 index 7974e62..0000000 --- a/src/main/java/org/decepticons/linkshortener/api/dto/NoSuchLinkFoundResponseDto.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.decepticons.linkshortener.api.dto; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; - -/** - * Data Transfer Object (DTO) representing the response when a requested link is not found. - * This object is typically returned in the response body of an API when a link-related - * operation fails due to the short link not existing in the system. - - * Example JSON: - * { - * "shortLink": "someShortLink", - * "message": "No such short link found in the system" - * } - */ -@AllArgsConstructor -@Getter -@Setter -public class NoSuchLinkFoundResponseDto { - - private String shortLink; - private String message; - -} diff --git a/src/main/java/org/decepticons/linkshortener/api/dto/NoSuchUserFoundResponseDto.java b/src/main/java/org/decepticons/linkshortener/api/dto/NoSuchUserFoundResponseDto.java deleted file mode 100644 index 16eee9f..0000000 --- a/src/main/java/org/decepticons/linkshortener/api/dto/NoSuchUserFoundResponseDto.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.decepticons.linkshortener.api.dto; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; - -/** - * Data Transfer Object (DTO) representing the response when a requested user is not found. - * This object is typically returned in the response body of an API when a user-related - * operation fails due to the user not existing in the system. - - * Example JSON: - * { - * "username": "Zakhar", - * "message": "No such user found in the system" - * } - */ -@AllArgsConstructor -@Getter -@Setter -public class NoSuchUserFoundResponseDto { - - private String username; - private String message; - -} diff --git a/src/main/java/org/decepticons/linkshortener/api/dto/ShortLinkOutOfDateResponseDto.java b/src/main/java/org/decepticons/linkshortener/api/dto/ShortLinkOutOfDateResponseDto.java deleted file mode 100644 index cbd17d0..0000000 --- a/src/main/java/org/decepticons/linkshortener/api/dto/ShortLinkOutOfDateResponseDto.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.decepticons.linkshortener.api.dto; - -import java.time.Instant; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; - - -/** - * Data Transfer Object (DTO) representing the response when a requested link is out of date. - * This object is typically returned in the response body of an API when a link-related - * operation fails due to the short link being obsolete in the system. - */ -@AllArgsConstructor -@Getter -@Setter -public class ShortLinkOutOfDateResponseDto { - private String shortLink; - private String message; - private Instant expiredAt; -} diff --git a/src/main/java/org/decepticons/linkshortener/api/exceptions/BaseException.java b/src/main/java/org/decepticons/linkshortener/api/exception/BaseException.java similarity index 92% rename from src/main/java/org/decepticons/linkshortener/api/exceptions/BaseException.java rename to src/main/java/org/decepticons/linkshortener/api/exception/BaseException.java index 864658f..277c444 100644 --- a/src/main/java/org/decepticons/linkshortener/api/exceptions/BaseException.java +++ b/src/main/java/org/decepticons/linkshortener/api/exception/BaseException.java @@ -1,4 +1,4 @@ -package org.decepticons.linkshortener.api.exceptions; +package org.decepticons.linkshortener.api.exception; /** * Base class for custom exceptions in the application. diff --git a/src/main/java/org/decepticons/linkshortener/api/exception/CustomizedResponseEntityExceptionHandler.java b/src/main/java/org/decepticons/linkshortener/api/exception/CustomizedResponseEntityExceptionHandler.java deleted file mode 100644 index e18c4e0..0000000 --- a/src/main/java/org/decepticons/linkshortener/api/exception/CustomizedResponseEntityExceptionHandler.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.decepticons.linkshortener.api.exception; - -import org.decepticons.linkshortener.api.dto.NoSuchLinkFoundResponseDto; -import org.decepticons.linkshortener.api.dto.NoSuchUserFoundResponseDto; -import org.decepticons.linkshortener.api.dto.ShortLinkOutOfDateResponseDto; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; - -/** - * Global exception handler for the application. - * Handles custom exceptions and transforms them into appropriate HTTP responses. - */ -@ControllerAdvice -public class CustomizedResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { - - /** - * Handles the case when a user is not found in the system. - * Returns a 404 Not Found response with a descriptive message. - */ - @ExceptionHandler(NoSuchUserFoundInTheSystemException.class) - public ResponseEntity handleNoSuchUserFoundInTheSystem( - NoSuchUserFoundInTheSystemException ex) { - - NoSuchUserFoundResponseDto response = new NoSuchUserFoundResponseDto( - ex.getUsername(), - "No such user found in the system" - ); - - return ResponseEntity.status(404).body(response); - } - - - /** - * Handles the case when a short link is not found in the system. - * Returns a 404 Not Found response with a descriptive message. - */ - @ExceptionHandler(NoSuchShortLinkFoundInTheSystemException.class) - public ResponseEntity handleNoSuchShortLinkFoundInTheSystem( - NoSuchShortLinkFoundInTheSystemException ex) { - - NoSuchLinkFoundResponseDto response = new NoSuchLinkFoundResponseDto( - ex.getShortLink(), - "No such short link found in the system" - ); - - return ResponseEntity.status(404).body(response); - } - - - /** - * Handles the case when a short link is out of date (expired). - * Returns a 410 Gone response with details about the expired link. - */ - @ExceptionHandler(ShortLinkIsOutOfDateException.class) - public ResponseEntity handleShortLinkIsOutOfDate( - ShortLinkIsOutOfDateException ex - ) { - ShortLinkOutOfDateResponseDto response = new ShortLinkOutOfDateResponseDto( - ex.getShortLink(), - "The short link is out of date", - ex.getExpiredAt() - ); - - return ResponseEntity.status(410).body(response); - } - - -} diff --git a/src/main/java/org/decepticons/linkshortener/api/exceptions/ExpiredTokenException.java b/src/main/java/org/decepticons/linkshortener/api/exception/ExpiredTokenException.java similarity index 84% rename from src/main/java/org/decepticons/linkshortener/api/exceptions/ExpiredTokenException.java rename to src/main/java/org/decepticons/linkshortener/api/exception/ExpiredTokenException.java index db24910..2b92426 100644 --- a/src/main/java/org/decepticons/linkshortener/api/exceptions/ExpiredTokenException.java +++ b/src/main/java/org/decepticons/linkshortener/api/exception/ExpiredTokenException.java @@ -1,4 +1,6 @@ -package org.decepticons.linkshortener.api.exceptions; +package org.decepticons.linkshortener.api.exception; + +import org.decepticons.linkshortener.api.exception.BaseException; /** * Thrown when a JWT token is expired. diff --git a/src/main/java/org/decepticons/linkshortener/api/exceptions/InvalidPasswordException.java b/src/main/java/org/decepticons/linkshortener/api/exception/InvalidPasswordException.java similarity index 75% rename from src/main/java/org/decepticons/linkshortener/api/exceptions/InvalidPasswordException.java rename to src/main/java/org/decepticons/linkshortener/api/exception/InvalidPasswordException.java index f3fe643..778c485 100644 --- a/src/main/java/org/decepticons/linkshortener/api/exceptions/InvalidPasswordException.java +++ b/src/main/java/org/decepticons/linkshortener/api/exception/InvalidPasswordException.java @@ -1,4 +1,6 @@ -package org.decepticons.linkshortener.api.exceptions; +package org.decepticons.linkshortener.api.exception; + +import org.decepticons.linkshortener.api.exception.BaseException; /** * Thrown when a password does not meet complexity requirements. diff --git a/src/main/java/org/decepticons/linkshortener/api/exceptions/InvalidTokenException.java b/src/main/java/org/decepticons/linkshortener/api/exception/InvalidTokenException.java similarity index 84% rename from src/main/java/org/decepticons/linkshortener/api/exceptions/InvalidTokenException.java rename to src/main/java/org/decepticons/linkshortener/api/exception/InvalidTokenException.java index 026aec4..e23a2e5 100644 --- a/src/main/java/org/decepticons/linkshortener/api/exceptions/InvalidTokenException.java +++ b/src/main/java/org/decepticons/linkshortener/api/exception/InvalidTokenException.java @@ -1,4 +1,6 @@ -package org.decepticons.linkshortener.api.exceptions; +package org.decepticons.linkshortener.api.exception; + +import org.decepticons.linkshortener.api.exception.BaseException; /** * Thrown when a JWT token is missing, malformed, or invalid. diff --git a/src/main/java/org/decepticons/linkshortener/api/exceptions/UserAlreadyExistsException.java b/src/main/java/org/decepticons/linkshortener/api/exception/UserAlreadyExistsException.java similarity index 78% rename from src/main/java/org/decepticons/linkshortener/api/exceptions/UserAlreadyExistsException.java rename to src/main/java/org/decepticons/linkshortener/api/exception/UserAlreadyExistsException.java index e0bd017..f4b008e 100644 --- a/src/main/java/org/decepticons/linkshortener/api/exceptions/UserAlreadyExistsException.java +++ b/src/main/java/org/decepticons/linkshortener/api/exception/UserAlreadyExistsException.java @@ -1,4 +1,6 @@ -package org.decepticons.linkshortener.api.exceptions; +package org.decepticons.linkshortener.api.exception; + +import org.decepticons.linkshortener.api.exception.BaseException; /** * Thrown when trying to register a user with a username that already exists. diff --git a/src/main/java/org/decepticons/linkshortener/api/exceptions/UserNotFoundException.java b/src/main/java/org/decepticons/linkshortener/api/exception/UserNotFoundException.java similarity index 76% rename from src/main/java/org/decepticons/linkshortener/api/exceptions/UserNotFoundException.java rename to src/main/java/org/decepticons/linkshortener/api/exception/UserNotFoundException.java index 53271a0..e804616 100644 --- a/src/main/java/org/decepticons/linkshortener/api/exceptions/UserNotFoundException.java +++ b/src/main/java/org/decepticons/linkshortener/api/exception/UserNotFoundException.java @@ -1,4 +1,6 @@ -package org.decepticons.linkshortener.api.exceptions; +package org.decepticons.linkshortener.api.exception; + +import org.decepticons.linkshortener.api.exception.BaseException; /** * Thrown when a user is not found in the system. diff --git a/src/main/java/org/decepticons/linkshortener/api/exceptions/package-info.java b/src/main/java/org/decepticons/linkshortener/api/exception/package-info.java similarity index 57% rename from src/main/java/org/decepticons/linkshortener/api/exceptions/package-info.java rename to src/main/java/org/decepticons/linkshortener/api/exception/package-info.java index 6f3cb2c..6286503 100644 --- a/src/main/java/org/decepticons/linkshortener/api/exceptions/package-info.java +++ b/src/main/java/org/decepticons/linkshortener/api/exception/package-info.java @@ -1,4 +1,4 @@ /** * Provides custom exception classes for the link shortener API. */ -package org.decepticons.linkshortener.api.exceptions; +package org.decepticons.linkshortener.api.exception; diff --git a/src/main/java/org/decepticons/linkshortener/api/security/jwt/JwtAuthenticationFilter.java b/src/main/java/org/decepticons/linkshortener/api/security/jwt/JwtAuthenticationFilter.java index 75c95da..5dddb72 100644 --- a/src/main/java/org/decepticons/linkshortener/api/security/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/org/decepticons/linkshortener/api/security/jwt/JwtAuthenticationFilter.java @@ -5,7 +5,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; -import org.decepticons.linkshortener.api.exceptions.InvalidTokenException; +import org.decepticons.linkshortener.api.exception.InvalidTokenException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; diff --git a/src/main/java/org/decepticons/linkshortener/api/security/service/impl/AuthServiceImpl.java b/src/main/java/org/decepticons/linkshortener/api/security/service/impl/AuthServiceImpl.java index 64b65e4..d97253e 100644 --- a/src/main/java/org/decepticons/linkshortener/api/security/service/impl/AuthServiceImpl.java +++ b/src/main/java/org/decepticons/linkshortener/api/security/service/impl/AuthServiceImpl.java @@ -5,8 +5,8 @@ import org.decepticons.linkshortener.api.dto.AuthRequestDto; import org.decepticons.linkshortener.api.dto.AuthResponseDto; import org.decepticons.linkshortener.api.dto.RegistrationRequestDto; -import org.decepticons.linkshortener.api.exceptions.ExpiredTokenException; -import org.decepticons.linkshortener.api.exceptions.InvalidTokenException; +import org.decepticons.linkshortener.api.exception.ExpiredTokenException; +import org.decepticons.linkshortener.api.exception.InvalidTokenException; import org.decepticons.linkshortener.api.model.User; import org.decepticons.linkshortener.api.security.jwt.JwtTokenUtil; import org.decepticons.linkshortener.api.security.model.CustomUserDetails; diff --git a/src/main/java/org/decepticons/linkshortener/api/security/service/impl/UserAuthServiceImpl.java b/src/main/java/org/decepticons/linkshortener/api/security/service/impl/UserAuthServiceImpl.java index 42195a1..60fd31a 100644 --- a/src/main/java/org/decepticons/linkshortener/api/security/service/impl/UserAuthServiceImpl.java +++ b/src/main/java/org/decepticons/linkshortener/api/security/service/impl/UserAuthServiceImpl.java @@ -3,9 +3,9 @@ import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.decepticons.linkshortener.api.dto.RegistrationRequestDto; -import org.decepticons.linkshortener.api.exceptions.InvalidPasswordException; -import org.decepticons.linkshortener.api.exceptions.UserAlreadyExistsException; -import org.decepticons.linkshortener.api.exceptions.UserNotFoundException; +import org.decepticons.linkshortener.api.exception.InvalidPasswordException; +import org.decepticons.linkshortener.api.exception.UserAlreadyExistsException; +import org.decepticons.linkshortener.api.exception.UserNotFoundException; import org.decepticons.linkshortener.api.model.User; import org.decepticons.linkshortener.api.repository.UserRepository; import org.decepticons.linkshortener.api.security.service.UserAuthService; diff --git a/src/test/java/org/decepticons/linkshortener/api/exceptions/GlobalExceptionHandlerTest.java b/src/test/java/org/decepticons/linkshortener/api/exceptions/GlobalExceptionHandlerTest.java index 35fd08c..53da06c 100644 --- a/src/test/java/org/decepticons/linkshortener/api/exceptions/GlobalExceptionHandlerTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/exceptions/GlobalExceptionHandlerTest.java @@ -1,5 +1,7 @@ package org.decepticons.linkshortener.api.exceptions; +import org.decepticons.linkshortener.api.controller.GlobalExceptionHandlerController; +import org.decepticons.linkshortener.api.exception.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -28,7 +30,7 @@ void setup() { // Given mockMvc = MockMvcBuilders .standaloneSetup(new TestController()) - .setControllerAdvice(new GlobalExceptionHandler()) + .setControllerAdvice(new GlobalExceptionHandlerController()) .build(); } diff --git a/src/test/java/org/decepticons/linkshortener/api/security/jwt/JwtAuthenticationFilterTest.java b/src/test/java/org/decepticons/linkshortener/api/security/jwt/JwtAuthenticationFilterTest.java index bde8a0a..06787a3 100644 --- a/src/test/java/org/decepticons/linkshortener/api/security/jwt/JwtAuthenticationFilterTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/security/jwt/JwtAuthenticationFilterTest.java @@ -4,7 +4,7 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.decepticons.linkshortener.api.exceptions.InvalidTokenException; +import org.decepticons.linkshortener.api.exception.InvalidTokenException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/org/decepticons/linkshortener/api/security/service/impl/AuthServiceImplTest.java b/src/test/java/org/decepticons/linkshortener/api/security/service/impl/AuthServiceImplTest.java index 33a2017..fef683e 100644 --- a/src/test/java/org/decepticons/linkshortener/api/security/service/impl/AuthServiceImplTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/security/service/impl/AuthServiceImplTest.java @@ -3,10 +3,10 @@ import org.decepticons.linkshortener.api.dto.AuthRequestDto; import org.decepticons.linkshortener.api.dto.AuthResponseDto; import org.decepticons.linkshortener.api.dto.RegistrationRequestDto; -import org.decepticons.linkshortener.api.exceptions.ExpiredTokenException; -import org.decepticons.linkshortener.api.exceptions.InvalidTokenException; -import org.decepticons.linkshortener.api.exceptions.UserAlreadyExistsException; -import org.decepticons.linkshortener.api.exceptions.UserNotFoundException; +import org.decepticons.linkshortener.api.exception.ExpiredTokenException; +import org.decepticons.linkshortener.api.exception.InvalidTokenException; +import org.decepticons.linkshortener.api.exception.UserAlreadyExistsException; +import org.decepticons.linkshortener.api.exception.UserNotFoundException; import org.decepticons.linkshortener.api.model.User; import org.decepticons.linkshortener.api.security.jwt.JwtTokenUtil; import org.decepticons.linkshortener.api.security.service.UserAuthService; diff --git a/src/test/java/org/decepticons/linkshortener/api/security/service/impl/UserAuthServiceImplTest.java b/src/test/java/org/decepticons/linkshortener/api/security/service/impl/UserAuthServiceImplTest.java index d3b6b8b..cda5b3e 100644 --- a/src/test/java/org/decepticons/linkshortener/api/security/service/impl/UserAuthServiceImplTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/security/service/impl/UserAuthServiceImplTest.java @@ -1,9 +1,9 @@ package org.decepticons.linkshortener.api.security.service.impl; import org.decepticons.linkshortener.api.dto.RegistrationRequestDto; -import org.decepticons.linkshortener.api.exceptions.InvalidPasswordException; -import org.decepticons.linkshortener.api.exceptions.UserAlreadyExistsException; -import org.decepticons.linkshortener.api.exceptions.UserNotFoundException; +import org.decepticons.linkshortener.api.exception.InvalidPasswordException; +import org.decepticons.linkshortener.api.exception.UserAlreadyExistsException; +import org.decepticons.linkshortener.api.exception.UserNotFoundException; import org.decepticons.linkshortener.api.model.User; import org.decepticons.linkshortener.api.repository.UserRepository; import org.junit.jupiter.api.BeforeEach; From 6a14aecdd4c2aee1aefb9b1f0df63a0c92a265dd Mon Sep 17 00:00:00 2001 From: zkhr Date: Mon, 15 Sep 2025 17:03:51 +0200 Subject: [PATCH 04/11] added User service --- .../api/service/LinkService.java | 37 +++++----------- .../api/service/UserService.java | 42 +++++++++++++++++++ .../api/v1/controller/LinkCrudController.java | 1 + .../controller/LinkCrudControllerTest.java | 1 + .../api/service/LinkServiceMethodsTest.java | 16 ++++--- .../api/service/LinkServiceTest.java | 6 +-- 6 files changed, 68 insertions(+), 35 deletions(-) create mode 100644 src/main/java/org/decepticons/linkshortener/api/service/UserService.java diff --git a/src/main/java/org/decepticons/linkshortener/api/service/LinkService.java b/src/main/java/org/decepticons/linkshortener/api/service/LinkService.java index 8fd4e96..61c4359 100644 --- a/src/main/java/org/decepticons/linkshortener/api/service/LinkService.java +++ b/src/main/java/org/decepticons/linkshortener/api/service/LinkService.java @@ -42,8 +42,9 @@ public class LinkService { private long linkExpirationDays; private final LinkRepository linkRepository; - private final UserRepository userRepository; + private final CacheEvictService cacheEvictService; + private final UserService userService; private final Random random = new Random(); @@ -54,11 +55,11 @@ public class LinkService { */ public LinkService(LinkRepository linkRepository, - UserRepository userRepository, - CacheEvictService cacheEvictService) { + CacheEvictService cacheEvictService, + UserService userService) { this.linkRepository = linkRepository; - this.userRepository = userRepository; this.cacheEvictService = cacheEvictService; + this.userService = userService; } @@ -202,7 +203,7 @@ public boolean validateLink(LinkResponseDto link) { * @return a {@link Page} of {@link LinkResponseDto} objects representing all user's links */ public Page getAllMyLinks(int page, int size) { - UUID userId = getCurrentUserId(); + UUID userId = userService.getCurrentUserId(); Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending()); return linkRepository.findAllByOwnerId(userId, pageable) .map(this::mapToResponse); @@ -216,7 +217,7 @@ public Page getAllMyLinks(int page, int size) { * @return a {@link Page} of {@link LinkResponseDto} objects representing active user's links */ public Page getAllMyActiveLinks(int page, int size) { - UUID userId = getCurrentUserId(); + UUID userId = userService.getCurrentUserId(); Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending()); return linkRepository.findAllByOwnerIdAndStatus(userId, LinkStatus.ACTIVE, pageable) .map(this::mapToResponse); @@ -229,9 +230,8 @@ public Page getAllMyActiveLinks(int page, int size) { */ @Transactional public void deleteLink(UUID linkId) { - UUID currentUserId = getCurrentUserId(); + UUID currentUserId = userService.getCurrentUserId(); - getCurrentUserId(); Link link = linkRepository.findById(linkId) .orElseThrow(() -> new NoSuchShortLinkFoundInTheSystemException( "No such short link found in the system", linkId.toString() @@ -251,24 +251,9 @@ public void deleteLink(UUID linkId) { * * @return the UUID of the authenticated user */ - private UUID getCurrentUserId() { - String username = SecurityContextHolder.getContext().getAuthentication().getName(); - User user = userRepository.findByUsername(username) - .orElseThrow(() -> new NoSuchUserFoundInTheSystemException( - "No such user found in the system: " + username, - username - )); - return user.getId(); - } - public User getCurrentUser() { - String username = SecurityContextHolder.getContext().getAuthentication().getName(); - return userRepository.findByUsername(username) - .orElseThrow(() -> new NoSuchUserFoundInTheSystemException( - "No such user found in the system: " + username, - username - )); - } + + @CachePut(value = "shortLinksCache", key = "#code") public LinkResponseDto updateLinkExpiration(String code, Instant newExpirationDate) { @@ -278,7 +263,7 @@ public LinkResponseDto updateLinkExpiration(String code, Instant newExpirationDa code )); - if (!link.getOwner().getId().equals(getCurrentUserId())) { + if (!link.getOwner().getId().equals(userService.getCurrentUserId())) { throw new AccessDeniedException("You are not allowed to update this link"); } diff --git a/src/main/java/org/decepticons/linkshortener/api/service/UserService.java b/src/main/java/org/decepticons/linkshortener/api/service/UserService.java new file mode 100644 index 0000000..6880ac1 --- /dev/null +++ b/src/main/java/org/decepticons/linkshortener/api/service/UserService.java @@ -0,0 +1,42 @@ +package org.decepticons.linkshortener.api.service; + +import org.decepticons.linkshortener.api.exception.NoSuchUserFoundInTheSystemException; +import org.decepticons.linkshortener.api.model.User; +import org.decepticons.linkshortener.api.repository.UserRepository; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@Service +public class UserService { + + public UserRepository userRepository; + + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public UUID getCurrentUserId() { + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new NoSuchUserFoundInTheSystemException( + "No such user found in the system: " + username, + username + )); + return user.getId(); + + } + + public User getCurrentUser() { + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + return userRepository.findByUsername(username) + .orElseThrow(() -> new NoSuchUserFoundInTheSystemException( + "No such user found in the system: " + username, + username + )); + } + + + +} diff --git a/src/main/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudController.java b/src/main/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudController.java index e1e55bf..2b39ac8 100644 --- a/src/main/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudController.java +++ b/src/main/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudController.java @@ -10,6 +10,7 @@ import org.decepticons.linkshortener.api.dto.UrlRequestDto; import org.decepticons.linkshortener.api.model.User; import org.decepticons.linkshortener.api.service.LinkService; +import org.decepticons.linkshortener.api.service.UserService; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; diff --git a/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java b/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java index a7c4f21..1426bc8 100644 --- a/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java @@ -7,6 +7,7 @@ import org.decepticons.linkshortener.api.exception.NoSuchUserFoundInTheSystemException; import org.decepticons.linkshortener.api.model.User; import org.decepticons.linkshortener.api.service.LinkService; +import org.decepticons.linkshortener.api.service.UserService; import org.decepticons.linkshortener.api.v1.controller.LinkCrudController; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceMethodsTest.java b/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceMethodsTest.java index 2105078..6425963 100644 --- a/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceMethodsTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceMethodsTest.java @@ -35,7 +35,7 @@ public class LinkServiceMethodsTest { private LinkRepository linkRepository; @Mock - private UserRepository userRepository; + private UserService userService; @Mock private CacheEvictService cacheEvictService; @@ -57,7 +57,6 @@ void setUpSecurityContext() { User testUser = new User(); testUser.setId(testUserId); testUser.setUsername(testUsername); - when(userRepository.findByUsername(testUsername)).thenReturn(Optional.of(testUser)); } @AfterEach @@ -69,10 +68,12 @@ void clearSecurityContext() { @DisplayName("Get All My Links - Success") void getAllMyLinks_SUCCESS() { Link link1 = new Link(); - link1.setOwner(userRepository.findByUsername(testUsername).get()); + link1.setOwner(new User()); Page mockPage = new PageImpl<>(List.of(link1)); + + when(userService.getCurrentUserId()).thenReturn(testUserId); when(linkRepository.findAllByOwnerId(eq(testUserId), any(Pageable.class))) .thenReturn(mockPage); @@ -88,10 +89,11 @@ void getAllMyLinks_SUCCESS() { void getAllMyActiveLinks_SUCCESS() { Link link1 = new Link(); link1.setStatus(LinkStatus.ACTIVE); - link1.setOwner(userRepository.findByUsername(testUsername).get()); + link1.setOwner(new User()); Page mockPage = new PageImpl<>(List.of(link1)); + when(userService.getCurrentUserId()).thenReturn(testUserId); when(linkRepository.findAllByOwnerIdAndStatus(eq(testUserId), eq(LinkStatus.ACTIVE), any(Pageable.class))) .thenReturn(mockPage); @@ -106,10 +108,14 @@ void getAllMyActiveLinks_SUCCESS() { @DisplayName("Delete Link - Success") void deleteLink_SUCCESS() { UUID linkId = UUID.randomUUID(); + User owner = new User(); + owner.setId(testUserId); + owner.setUsername(testUsername); Link link = new Link(); link.setCode("abc123"); - link.setOwner(userRepository.findByUsername(testUsername).get()); + link.setOwner(owner); + when(userService.getCurrentUserId()).thenReturn(testUserId); when(linkRepository.findById(linkId)).thenReturn(Optional.of(link)); linkService.deleteLink(linkId); diff --git a/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceTest.java b/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceTest.java index ab6e4e6..41a6f61 100644 --- a/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceTest.java @@ -38,7 +38,7 @@ class LinkServiceTest { private LinkRepository linkRepository; @Mock - private UserRepository userRepository; + private UserService userService; @InjectMocks private LinkService linkService; @@ -219,13 +219,11 @@ void testUpdateLinkExpiration(){ Authentication auth = mock(Authentication.class); SecurityContext securityContext = mock(SecurityContext.class); - when(securityContext.getAuthentication()).thenReturn(auth); - when(auth.getName()).thenReturn("testUser"); SecurityContextHolder.setContext(securityContext); when(linkRepository.findByCode(code)).thenReturn(Optional.of(link)); when(linkRepository.save(any(Link.class))).thenAnswer(i -> i.getArguments()[0]); - when(userRepository.findByUsername("testUser")).thenReturn(Optional.of(owner)); + when(userService.getCurrentUserId()).thenReturn(owner.getId()); LinkResponseDto response = linkService.updateLinkExpiration(code, Instant .now() From 54a10bdb77a03898fa21417acb5c30c2bb79f239 Mon Sep 17 00:00:00 2001 From: zkhr Date: Mon, 15 Sep 2025 17:04:10 +0200 Subject: [PATCH 05/11] added User service --- .../api/v1/controller/LinkCrudController.java | 7 ++++--- .../api/controller/LinkCrudControllerTest.java | 9 ++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudController.java b/src/main/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudController.java index 2b39ac8..cec2d5e 100644 --- a/src/main/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudController.java +++ b/src/main/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudController.java @@ -27,15 +27,16 @@ public class LinkCrudController { private final LinkService linkService; - + private final UserService userService; /** * Constructs a new {@link LinkCrudController} with the given dependencies. * * @param linkService the service responsible for link business logic */ - public LinkCrudController(LinkService linkService) { + public LinkCrudController(LinkService linkService, UserService userService) { this.linkService = linkService; + this.userService = userService; } @@ -48,7 +49,7 @@ public LinkCrudController(LinkService linkService) { @PostMapping @Operation(summary = "Create a short URL for the current user") public ResponseEntity createLink(@Valid @RequestBody UrlRequestDto originalUrl) { - User user = linkService.getCurrentUser(); + User user = userService.getCurrentUser(); LinkResponseDto link = linkService.createLink(originalUrl, user); diff --git a/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java b/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java index 1426bc8..b7cc9a7 100644 --- a/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java @@ -40,6 +40,9 @@ class LinkCrudControllerTest { @Mock private LinkService linkService; + @Mock + UserService userService; + @InjectMocks private LinkCrudController linkController; @@ -76,7 +79,7 @@ void testCreateShortLink_Success() { fakeUser.getId() ); - when(linkService.getCurrentUser()).thenReturn(fakeUser); + when(userService.getCurrentUser()).thenReturn(fakeUser); when(linkService.createLink(urlRequestDto, fakeUser)).thenReturn(fakeResponse); @@ -90,7 +93,7 @@ void testCreateShortLink_Success() { assertEquals("https://example.com/some/long/url", response.getBody().originalUrl()); - verify(linkService).getCurrentUser(); + verify(userService).getCurrentUser(); verify(linkService).createLink(urlRequestDto, fakeUser); } @@ -104,7 +107,7 @@ void testCreateShortLink_UserNotFound() { urlRequestDto.setUrl("https://example.com/some/long/url"); - when(linkService.getCurrentUser()) + when(userService.getCurrentUser()) .thenThrow(new NoSuchUserFoundInTheSystemException("No such user", "someName")); From 60faefe8f04e4271eac7a7094ecca38f452a924c Mon Sep 17 00:00:00 2001 From: zkhr Date: Mon, 15 Sep 2025 20:30:40 +0200 Subject: [PATCH 06/11] fixed logical problems --- .../GlobalExceptionHandlerController.java | 80 ++++- .../api/controller/RedirectController.java | 21 +- .../api/dto/ApiErrorResponseDto.java | 19 -- .../InvalidExpirationDateException.java | 15 +- .../api/repository/LinkRepository.java | 12 + .../api/security/config/SecurityConfig.java | 4 +- .../api/service/CacheEvictService.java | 19 -- .../api/service/CacheInspectionService.java | 45 --- .../api/service/LinkService.java | 280 +++--------------- .../api/service/UserService.java | 55 ++-- .../api/v1/controller/CacheController.java | 40 --- .../api/v1/controller/LinkCrudController.java | 30 +- src/main/resources/application.yaml | 8 +- .../controller/LinkCrudControllerTest.java | 42 ++- .../controller/RedirectControllerTest.java | 17 +- ...t.java => LinkServiceImplMethodsTest.java} | 18 +- ...viceTest.java => LinkServiceImplTest.java} | 38 ++- .../api/service/UserServiceImplTest.java | 98 ++++++ .../v1/controller/LinkCrudControllerTest.java | 18 +- 19 files changed, 360 insertions(+), 499 deletions(-) delete mode 100644 src/main/java/org/decepticons/linkshortener/api/dto/ApiErrorResponseDto.java delete mode 100644 src/main/java/org/decepticons/linkshortener/api/service/CacheEvictService.java delete mode 100644 src/main/java/org/decepticons/linkshortener/api/service/CacheInspectionService.java delete mode 100644 src/main/java/org/decepticons/linkshortener/api/v1/controller/CacheController.java rename src/test/java/org/decepticons/linkshortener/api/service/{LinkServiceMethodsTest.java => LinkServiceImplMethodsTest.java} (87%) rename src/test/java/org/decepticons/linkshortener/api/service/{LinkServiceTest.java => LinkServiceImplTest.java} (84%) create mode 100644 src/test/java/org/decepticons/linkshortener/api/service/UserServiceImplTest.java diff --git a/src/main/java/org/decepticons/linkshortener/api/controller/GlobalExceptionHandlerController.java b/src/main/java/org/decepticons/linkshortener/api/controller/GlobalExceptionHandlerController.java index f9519d5..949062f 100644 --- a/src/main/java/org/decepticons/linkshortener/api/controller/GlobalExceptionHandlerController.java +++ b/src/main/java/org/decepticons/linkshortener/api/controller/GlobalExceptionHandlerController.java @@ -2,16 +2,37 @@ import java.time.Instant; import java.util.Map; - -import org.decepticons.linkshortener.api.exception.*; +import org.decepticons.linkshortener.api.exception.ExpiredTokenException; +import org.decepticons.linkshortener.api.exception.InvalidPasswordException; +import org.decepticons.linkshortener.api.exception.InvalidTokenException; +import org.decepticons.linkshortener.api.exception.NoSuchShortLinkFoundInTheSystemException; +import org.decepticons.linkshortener.api.exception.NoSuchUserFoundInTheSystemException; +import org.decepticons.linkshortener.api.exception.ShortLinkIsOutOfDateException; +import org.decepticons.linkshortener.api.exception.UserAlreadyExistsException; +import org.decepticons.linkshortener.api.exception.UserNotFoundException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; + + +/** + * Global exception handler for managing application-wide exceptions. + */ @ControllerAdvice public class GlobalExceptionHandlerController { + + /** + * Builds a standardized error response. + * + * @param status The HTTP status to be returned. + * @param error A brief description of the error. + * @param message A detailed message about the error. + * @param details Additional details about the error. + * @return A ResponseEntity containing the error details. + */ private ResponseEntity> buildErrorResponse( final HttpStatus status, final String error, @@ -26,6 +47,16 @@ private ResponseEntity> buildErrorResponse( )); } + /** + * Builds a standardized error response. + * + * @param status The HTTP status to be returned. + * @param error A brief description of the error. + * @param message A detailed message about the error. + * @return A ResponseEntity containing the error details. + */ + + private ResponseEntity> buildErrorResponseSecurity( final HttpStatus status, final String error, @@ -126,29 +157,62 @@ public ResponseEntity> handleGeneric(final Exception ex) { ex.getMessage()); } - // === Link Shortener специфичные ошибки === + + /** + * Handles exceptions when no such user is found in the system. + * + * @param ex the exception instance + * + * @return a ResponseEntity with error details + */ + @ExceptionHandler(NoSuchUserFoundInTheSystemException.class) - public ResponseEntity> handleNoSuchUser(NoSuchUserFoundInTheSystemException ex) { + public ResponseEntity> handleNoSuchUser( + NoSuchUserFoundInTheSystemException ex) { + return buildErrorResponse( HttpStatus.NOT_FOUND, "User Not Found", "No such user in the system", - Map.of("username", ex.getUsername()) + Map.of( + "username", ex.getUsername() + ) ); } + /** + * Handles exceptions when no such short link is found in the system. + * + * @param ex Exception instance + * + * @return Response entity with error details + */ + @ExceptionHandler(NoSuchShortLinkFoundInTheSystemException.class) - public ResponseEntity> handleNoSuchLink(NoSuchShortLinkFoundInTheSystemException ex) { + public ResponseEntity> handleNoSuchLink( + NoSuchShortLinkFoundInTheSystemException ex) { + return buildErrorResponse( HttpStatus.NOT_FOUND, "Short Link Not Found", "No such short link in the system", - Map.of("shortLink", ex.getShortLink()) + Map.of( + "shortLink", ex.getShortLink() + ) ); } + /** + * Handles exceptions when a short link is out of date. + * + * @param ex Exception instance + * + * @return Response entity with error details + */ + @ExceptionHandler(ShortLinkIsOutOfDateException.class) - public ResponseEntity> handleShortLinkOutOfDate(ShortLinkIsOutOfDateException ex) { + public ResponseEntity> handleShortLinkOutOfDate( + ShortLinkIsOutOfDateException ex) { return buildErrorResponse( HttpStatus.GONE, "Short Link Expired", diff --git a/src/main/java/org/decepticons/linkshortener/api/controller/RedirectController.java b/src/main/java/org/decepticons/linkshortener/api/controller/RedirectController.java index 06c2805..d1b3b88 100644 --- a/src/main/java/org/decepticons/linkshortener/api/controller/RedirectController.java +++ b/src/main/java/org/decepticons/linkshortener/api/controller/RedirectController.java @@ -2,13 +2,11 @@ import jakarta.servlet.http.HttpServletResponse; - import java.io.IOException; - import org.decepticons.linkshortener.api.dto.LinkResponseDto; import org.decepticons.linkshortener.api.exception.ShortLinkIsOutOfDateException; import org.decepticons.linkshortener.api.model.LinkStatus; -import org.decepticons.linkshortener.api.service.LinkService; +import org.decepticons.linkshortener.api.service.impl.LinkServiceImpl; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -17,23 +15,22 @@ /** * REST controller for redirecting short links. - *

* Provides an endpoint to redirect to the original URL based on the short code. */ @RestController @RequestMapping("/api/links") public class RedirectController { - private final LinkService linkService; + private final LinkServiceImpl linkServiceImpl; /** * Constructs a new {@link RedirectController} with the given dependencies. * - * @param linkService the service responsible for link business logic + * @param linkServiceImpl the service responsible for link business logic */ - public RedirectController(LinkService linkService) { - this.linkService = linkService; + public RedirectController(LinkServiceImpl linkServiceImpl) { + this.linkServiceImpl = linkServiceImpl; } @@ -46,13 +43,13 @@ public RedirectController(LinkService linkService) { public void redirect(@PathVariable String code, HttpServletResponse response) throws IOException { - LinkResponseDto linkByCode = linkService.getLinkByCode(code); + LinkResponseDto linkByCode = linkServiceImpl.getLinkByCode(code); - if (linkService.validateLink(linkByCode)) { - linkService.incrementClicks(linkByCode); + if (linkServiceImpl.validateLink(linkByCode)) { + linkByCode = linkServiceImpl.incrementClicks(linkByCode); } else { if (linkByCode.status().equalsIgnoreCase(LinkStatus.ACTIVE.name())) { - linkService.deactivateLink(linkByCode); + linkServiceImpl.deactivateLink(linkByCode); } throw new ShortLinkIsOutOfDateException( "Short link is out of date: " + code, diff --git a/src/main/java/org/decepticons/linkshortener/api/dto/ApiErrorResponseDto.java b/src/main/java/org/decepticons/linkshortener/api/dto/ApiErrorResponseDto.java deleted file mode 100644 index 7fd7a14..0000000 --- a/src/main/java/org/decepticons/linkshortener/api/dto/ApiErrorResponseDto.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.decepticons.linkshortener.api.dto; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; - -import java.time.LocalDateTime; - -@Getter -@Setter -@AllArgsConstructor -public class ApiErrorResponseDto { - private LocalDateTime timestamp; - private int status; - private String error; - private String message; - private String path; - private Object details; -} diff --git a/src/main/java/org/decepticons/linkshortener/api/exception/InvalidExpirationDateException.java b/src/main/java/org/decepticons/linkshortener/api/exception/InvalidExpirationDateException.java index bfca945..22244c8 100644 --- a/src/main/java/org/decepticons/linkshortener/api/exception/InvalidExpirationDateException.java +++ b/src/main/java/org/decepticons/linkshortener/api/exception/InvalidExpirationDateException.java @@ -1,16 +1,27 @@ package org.decepticons.linkshortener.api.exception; +import java.time.Instant; import lombok.Getter; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; -import java.time.Instant; - +/** + * Exception thrown when an invalid expiration date is provided for a link. + * This exception indicates that the provided expiration date does not meet + * the required criteria (e.g., it may be in the past or too far in the future). + */ @ResponseStatus(HttpStatus.BAD_REQUEST) @Getter public class InvalidExpirationDateException extends RuntimeException { private final Instant invalidDate; + /** + * Constructs a new InvalidExpirationDateException with the specified detail message + * and the invalid expiration date. + * + * @param message the detail message. + * @param invalidDate the invalid expiration date that caused this exception. + */ public InvalidExpirationDateException(String message, Instant invalidDate) { super(message); this.invalidDate = invalidDate; diff --git a/src/main/java/org/decepticons/linkshortener/api/repository/LinkRepository.java b/src/main/java/org/decepticons/linkshortener/api/repository/LinkRepository.java index 62cc634..534a75c 100644 --- a/src/main/java/org/decepticons/linkshortener/api/repository/LinkRepository.java +++ b/src/main/java/org/decepticons/linkshortener/api/repository/LinkRepository.java @@ -7,6 +7,9 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; /** @@ -51,4 +54,13 @@ public interface LinkRepository extends JpaRepository { * @return a {@link Page} of {@link Link} objects belonging to the user with the specified status */ Page findAllByOwnerIdAndStatus(UUID ownerId, LinkStatus status, Pageable pageable); + + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query(value = """ + UPDATE links + SET clicks = clicks + 1, last_accessed_at = now() + WHERE code = :code +""", nativeQuery = true) + int incrementClicksByCodeNative(@Param("code") String code); } \ No newline at end of file diff --git a/src/main/java/org/decepticons/linkshortener/api/security/config/SecurityConfig.java b/src/main/java/org/decepticons/linkshortener/api/security/config/SecurityConfig.java index def7356..34bb375 100644 --- a/src/main/java/org/decepticons/linkshortener/api/security/config/SecurityConfig.java +++ b/src/main/java/org/decepticons/linkshortener/api/security/config/SecurityConfig.java @@ -57,10 +57,10 @@ public SecurityFilterChain securityFilterChain( "/api/v1/auth/login", "/api/v1/auth/refresh", -// Redirect endpoint + // Redirect endpoint "/api/links/**", -// Documentation & health endpoints + // Documentation & health endpoints "/api/v1/cache", "/health", "/h2-console/**", diff --git a/src/main/java/org/decepticons/linkshortener/api/service/CacheEvictService.java b/src/main/java/org/decepticons/linkshortener/api/service/CacheEvictService.java deleted file mode 100644 index 9ffd6d8..0000000 --- a/src/main/java/org/decepticons/linkshortener/api/service/CacheEvictService.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.decepticons.linkshortener.api.service; - -import org.springframework.cache.annotation.CacheEvict; -import org.springframework.stereotype.Service; - -/** - * Service for evicting cache entries related to short links. - */ -@Service -public class CacheEvictService { - /** - * Evicts the cache entry for the given short link code. - * - * @param code the short link code whose cache entry should be evicted - */ - @CacheEvict(value = "shortLinksCache", key = "#code") - public void evictLink(String code) { // it's indeed should be empty, don't delete it :) - } -} diff --git a/src/main/java/org/decepticons/linkshortener/api/service/CacheInspectionService.java b/src/main/java/org/decepticons/linkshortener/api/service/CacheInspectionService.java deleted file mode 100644 index 6b29c93..0000000 --- a/src/main/java/org/decepticons/linkshortener/api/service/CacheInspectionService.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.decepticons.linkshortener.api.service; - - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; -import org.springframework.stereotype.Service; - -/** - * Service for inspecting and printing cache contents. - */ -@Service -public class CacheInspectionService { - - - private final CacheManager cacheManager; - private final Logger logger = LoggerFactory.getLogger(CacheInspectionService.class); - - /** - * Constructs a CacheInspectionService with the given CacheManager. - * - * @param cacheManager the CacheManager to use for accessing caches - */ - - public CacheInspectionService(CacheManager cacheManager) { - this.cacheManager = cacheManager; - } - - /** - * Prints the contents of the specified cache to the console. - * - * @param cacheName the name of the cache to inspect - */ - public void printCache(String cacheName) { - Cache cache = cacheManager.getCache(cacheName); - if (cache != null) { - logger.debug("Cache Name: {}", cacheName); - logger.debug("Cache Content: {}", cache.getNativeCache()); - } else { - logger.debug("Cache with name {} not found", cacheName); - } - } - -} diff --git a/src/main/java/org/decepticons/linkshortener/api/service/LinkService.java b/src/main/java/org/decepticons/linkshortener/api/service/LinkService.java index 61c4359..b80641c 100644 --- a/src/main/java/org/decepticons/linkshortener/api/service/LinkService.java +++ b/src/main/java/org/decepticons/linkshortener/api/service/LinkService.java @@ -1,283 +1,91 @@ package org.decepticons.linkshortener.api.service; import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Random; import java.util.UUID; - import org.decepticons.linkshortener.api.dto.LinkResponseDto; import org.decepticons.linkshortener.api.dto.UrlRequestDto; -import org.decepticons.linkshortener.api.exception.InvalidExpirationDateException; -import org.decepticons.linkshortener.api.exception.NoSuchShortLinkFoundInTheSystemException; -import org.decepticons.linkshortener.api.exception.NoSuchUserFoundInTheSystemException; -import org.decepticons.linkshortener.api.model.Link; -import org.decepticons.linkshortener.api.model.LinkStatus; -import org.decepticons.linkshortener.api.model.User; -import org.decepticons.linkshortener.api.repository.LinkRepository; -import org.decepticons.linkshortener.api.repository.UserRepository; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.cache.annotation.CachePut; -import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; + /** - * Application service responsible for creating and maintaining {@link Link} entities. - * Provides operations for link creation, click tracking, simple DTO mapping, - * and basic liveness checks (active + not expired). - *

+ * Service interface for managing links in the link shortener application. + * Provides methods for creating, retrieving, updating, and deleting links, + * as well as tracking link clicks and validating link status. */ - - -@Service -public class LinkService { - - @Value("${link.expiration-days}") - private long linkExpirationDays; - - private final LinkRepository linkRepository; - - private final CacheEvictService cacheEvictService; - private final UserService userService; - private final Random random = new Random(); - +public interface LinkService { /** - * Creates a new {@code LinkService}. + * Creates and persists a new shortened link based on the provided original URL. * - * @param linkRepository repository used to persist and load {@link Link} entities + * @param originalUrl the original URL to be shortened + * @return a LinkResponseDto containing details of the created shortened link */ - - public LinkService(LinkRepository linkRepository, - CacheEvictService cacheEvictService, - UserService userService) { - this.linkRepository = linkRepository; - this.cacheEvictService = cacheEvictService; - this.userService = userService; - } - + LinkResponseDto createLink(UrlRequestDto originalUrl); /** - * Creates and persists a new {@link Link}. + * Increments the click count for the specified link. * - * @param originalUrl the original long URL to be shortened - * @param owner the user who owns the link (must be non-null and managed) - * @return a {@link LinkResponseDto} representing the newly created link + * @param link the LinkResponseDto representing the link to update + * @return the updated LinkResponseDto with incremented click count */ - @Transactional - public LinkResponseDto createLink(UrlRequestDto originalUrl, User owner) { - Link link = new Link(); - link.setOriginalUrl(originalUrl.getUrl()); - link.setOwner(owner); - - link.setCode(generateRandomCode()); - link.setExpiresAt(Instant.now().plus(linkExpirationDays, ChronoUnit.DAYS)); - link.setStatus(LinkStatus.ACTIVE); - - linkRepository.save(link); - - return mapToResponse(link); - } - + LinkResponseDto incrementClicks(LinkResponseDto link); /** - * Increments the click counter of the given link and persists the change. - * This should be invoked whenever the shortened URL is accessed. - * Internally, the entity updates its {@code lastAccessedAt} timestamp. - *

+ * Retrieves a link by its unique short code. * - * @param link the link whose click counter should be incremented + * @param code the unique short code of the link + * @return the LinkResponseDto representing the retrieved link */ - @Transactional - @CachePut(value = "shortLinksCache", key = "#link.code") - public LinkResponseDto incrementClicks(LinkResponseDto link) { - Link linkByCode = linkRepository.findByCode(link.code()) - .orElseThrow(() -> new NoSuchShortLinkFoundInTheSystemException( - "No such short link found in the system: " + link.code(), - link.code() - )); - linkByCode.incrementClicks(); - linkRepository.save(linkByCode); - - return mapToResponse(linkByCode); - } - + LinkResponseDto getLinkByCode(String code); /** - * Maps a {@link Link} JPA entity to a transport-friendly {@link LinkResponseDto}. + * Deactivates the specified link, preventing further access. * - * @param link the entity to map - * @return a response DTO with the most relevant fields + * @param link the LinkResponseDto representing the link to deactivate + * @return the updated LinkResponseDto with deactivated status */ - public LinkResponseDto mapToResponse(Link link) { - return new LinkResponseDto( - - link.getId(), - link.getCode(), - link.getOriginalUrl(), - link.getCreatedAt(), - link.getExpiresAt(), - link.getClicks(), - link.getStatus().name(), - link.getOwner().getId() - ); - } - + LinkResponseDto deactivateLink(LinkResponseDto link); /** - * Generates a pseudo-random short code of fixed length . + * Validates whether the specified link is active and not expired. * - * @return a new short code (e.g., {@code "aZ3fQ1"}) + * @param link the LinkResponseDto representing the link to validate + * @return true if the link is valid (active and not expired), false otherwise */ - private String generateRandomCode() { - String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < 6; i++) { - sb.append(chars.charAt(random.nextInt(chars.length()))); - } - return sb.toString(); - } - + boolean validateLink(LinkResponseDto link); /** - * Retrieves a {@link Link} entity by its short code. + * Retrieves a paginated list of all links created by the currently authenticated user. * - * @param code short link code. - * @return Optional + * @param page the page number to retrieve + * @param size the number of links per page + * @return a Page of LinkResponseDto representing the user's links */ - @Cacheable(value = "shortLinksCache", key = "#code") - public LinkResponseDto getLinkByCode(String code) { - Link link = linkRepository.findByCode(code) - .orElseThrow(() -> new NoSuchShortLinkFoundInTheSystemException( - "No such short link found in the system: " + code, - code - )); - - return mapToResponse(link); - - } - + Page getAllMyLinks(int page, int size); /** - * Deactivates a link by setting its status to INACTIVE. + * Retrieves a paginated list of all active links created by the currently authenticated user. * - * @param link the link to deactivate - * @return the updated {@link LinkResponseDto} with status set to INACTIVE + * @param page the page number to retrieve + * @param size the number of links per page + * @return a Page of LinkResponseDto representing the user's active links */ - @CachePut(value = "shortLinksCache", key = "#link.code") - public LinkResponseDto deactivateLink(LinkResponseDto link) { - Link linkByCode = linkRepository.findByCode(link.code()) - .orElseThrow(() -> new NoSuchShortLinkFoundInTheSystemException( - "No such short link found in the system: " + link.code(), - link.code() - )); - linkByCode.setStatus(LinkStatus.INACTIVE); - linkRepository.save(linkByCode); - return mapToResponse(linkByCode); - } - + Page getAllMyActiveLinks(int page, int size); /** - * Validates if a link is active and not expired. + * Deletes a link by its unique identifier. * - * @param link the link to validate - * @return {@code true} if the link is active and not expired; {@code false} otherwise + * @param linkId the UUID of the link to delete + * @return a confirmation message indicating the result of the deletion */ - public boolean validateLink(LinkResponseDto link) { - return link.status().equalsIgnoreCase(LinkStatus.ACTIVE.toString()) - && (link.expiresAt() == null || link.expiresAt().isAfter(Instant.now())); - } - + String deleteLink(UUID linkId); /** - * Retrieves all links of the currently authenticated user with pagination. + * Updates the expiration date of a link identified by its short code. * - * @param page the page number to retrieve (0-based) - * @param size the number of records per page - * @return a {@link Page} of {@link LinkResponseDto} objects representing all user's links + * @param code the unique short code of the link to update + * @param newExpirationDate the new expiration date to set + * @return the updated LinkResponseDto with the new expiration date */ - public Page getAllMyLinks(int page, int size) { - UUID userId = userService.getCurrentUserId(); - Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending()); - return linkRepository.findAllByOwnerId(userId, pageable) - .map(this::mapToResponse); - } - - /** - * Retrieves all active links of the currently authenticated user with pagination. - * - * @param page the page number to retrieve (0-based) - * @param size the number of records per page - * @return a {@link Page} of {@link LinkResponseDto} objects representing active user's links - */ - public Page getAllMyActiveLinks(int page, int size) { - UUID userId = userService.getCurrentUserId(); - Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending()); - return linkRepository.findAllByOwnerIdAndStatus(userId, LinkStatus.ACTIVE, pageable) - .map(this::mapToResponse); - } - - /** - * Deletes a link from the database if it belongs to the currently authenticated user. - * - * @param linkId the unique identifier of the link to delete - */ - @Transactional - public void deleteLink(UUID linkId) { - UUID currentUserId = userService.getCurrentUserId(); - - Link link = linkRepository.findById(linkId) - .orElseThrow(() -> new NoSuchShortLinkFoundInTheSystemException( - "No such short link found in the system", linkId.toString() - )); - - - if (!link.getOwner().getId().equals(currentUserId)) { - throw new AccessDeniedException("You are not allowed to delete this link"); - } - - linkRepository.delete(link); - cacheEvictService.evictLink(link.getCode()); - } - - /** - * Retrieves the UUID of the currently authenticated user from the security context. - * - * @return the UUID of the authenticated user - */ - - - - - @CachePut(value = "shortLinksCache", key = "#code") - public LinkResponseDto updateLinkExpiration(String code, Instant newExpirationDate) { - Link link = linkRepository.findByCode(code) - .orElseThrow(() -> new NoSuchShortLinkFoundInTheSystemException( - "No such short link found in the system: " + code, - code - )); - - if (!link.getOwner().getId().equals(userService.getCurrentUserId())) { - throw new AccessDeniedException("You are not allowed to update this link"); - } - - if (newExpirationDate.isBefore(Instant.now())) { - throw new InvalidExpirationDateException( - "Expiration date must be in the future", - newExpirationDate - ); - } - - link.setExpiresAt(newExpirationDate); - linkRepository.save(link); - - return mapToResponse(link); - } + LinkResponseDto updateLinkExpiration(String code, Instant newExpirationDate); } - diff --git a/src/main/java/org/decepticons/linkshortener/api/service/UserService.java b/src/main/java/org/decepticons/linkshortener/api/service/UserService.java index 6880ac1..af05c4c 100644 --- a/src/main/java/org/decepticons/linkshortener/api/service/UserService.java +++ b/src/main/java/org/decepticons/linkshortener/api/service/UserService.java @@ -1,42 +1,25 @@ package org.decepticons.linkshortener.api.service; -import org.decepticons.linkshortener.api.exception.NoSuchUserFoundInTheSystemException; -import org.decepticons.linkshortener.api.model.User; -import org.decepticons.linkshortener.api.repository.UserRepository; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Service; - import java.util.UUID; - -@Service -public class UserService { - - public UserRepository userRepository; - - public UserService(UserRepository userRepository) { - this.userRepository = userRepository; - } - - public UUID getCurrentUserId() { - String username = SecurityContextHolder.getContext().getAuthentication().getName(); - User user = userRepository.findByUsername(username) - .orElseThrow(() -> new NoSuchUserFoundInTheSystemException( - "No such user found in the system: " + username, - username - )); - return user.getId(); - - } - - public User getCurrentUser() { - String username = SecurityContextHolder.getContext().getAuthentication().getName(); - return userRepository.findByUsername(username) - .orElseThrow(() -> new NoSuchUserFoundInTheSystemException( - "No such user found in the system: " + username, - username - )); - } - +import org.decepticons.linkshortener.api.model.User; +/** + * Service interface for retrieving information about the currently authenticated user. + */ +public interface UserService { + + /** + * Retrieves the currently authenticated user. + * + * @return the User object representing the current user + */ + User getCurrentUser(); + + /** + * Retrieves the unique identifier (UUID) of the currently authenticated user. + * + * @return the UUID of the current user + */ + UUID getCurrentUserId(); } diff --git a/src/main/java/org/decepticons/linkshortener/api/v1/controller/CacheController.java b/src/main/java/org/decepticons/linkshortener/api/v1/controller/CacheController.java deleted file mode 100644 index cf54335..0000000 --- a/src/main/java/org/decepticons/linkshortener/api/v1/controller/CacheController.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.decepticons.linkshortener.api.v1.controller; - -import org.decepticons.linkshortener.api.service.CacheInspectionService; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -/** - * REST controller for cache inspection. - * Provides an endpoint to inspect the contents of the short links cache. - * This is primarily for debugging and monitoring purposes. - * Note: In a production environment, access to this endpoint should be - * restricted to authorized personnel only, as it may expose sensitive data. - */ -@RestController -@RequestMapping("/api/v1/cache") -public class CacheController { - - private final CacheInspectionService cacheInspectionService; - - /** - * Constructs a new {@link CacheController} with the given dependencies. - * - * @param cacheInspectionService the service responsible for cache inspection - */ - public CacheController(CacheInspectionService cacheInspectionService) { - this.cacheInspectionService = cacheInspectionService; - } - - /** - * Endpoint to inspect the contents of the short links cache. - * This is primarily for debugging and monitoring purposes. - * Note: In a production environment, access to this endpoint should be - * restricted to authorized personnel only, as it may expose sensitive data. - */ - @GetMapping - public void inspectCache() { - cacheInspectionService.printCache("shortLinksCache"); - } -} diff --git a/src/main/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudController.java b/src/main/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudController.java index cec2d5e..b040eda 100644 --- a/src/main/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudController.java +++ b/src/main/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudController.java @@ -3,17 +3,24 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; - import java.util.UUID; import org.decepticons.linkshortener.api.dto.LinkResponseDto; import org.decepticons.linkshortener.api.dto.UpdateLinkExpirationRequestDto; import org.decepticons.linkshortener.api.dto.UrlRequestDto; -import org.decepticons.linkshortener.api.model.User; import org.decepticons.linkshortener.api.service.LinkService; -import org.decepticons.linkshortener.api.service.UserService; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + + /** @@ -27,16 +34,16 @@ public class LinkCrudController { private final LinkService linkService; - private final UserService userService; + /** * Constructs a new {@link LinkCrudController} with the given dependencies. * * @param linkService the service responsible for link business logic */ - public LinkCrudController(LinkService linkService, UserService userService) { + public LinkCrudController(LinkService linkService) { this.linkService = linkService; - this.userService = userService; + } @@ -49,9 +56,9 @@ public LinkCrudController(LinkService linkService, UserService userService) { @PostMapping @Operation(summary = "Create a short URL for the current user") public ResponseEntity createLink(@Valid @RequestBody UrlRequestDto originalUrl) { - User user = userService.getCurrentUser(); - LinkResponseDto link = linkService.createLink(originalUrl, user); + + LinkResponseDto link = linkService.createLink(originalUrl); return ResponseEntity.status(201).body(link); } @@ -116,8 +123,9 @@ public ResponseEntity updateLinkExpiration( @Valid @RequestBody UpdateLinkExpirationRequestDto newExpirationDate, @PathVariable String code) { - return ResponseEntity - .ok(linkService.updateLinkExpiration(code, newExpirationDate.getNewExpirationDate())); + return ResponseEntity + .ok(linkService.updateLinkExpiration(code, newExpirationDate.getNewExpirationDate())); } + } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 8fa7c4b..0593788 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -10,6 +10,9 @@ logging: level: org.springdoc: DEBUG + root: DEBUG # set global logging to DEBUG + org.springframework: INFO # optional: keep Spring framework logs at INFO to reduce noise + com.yourpackage: DEBUG # set your own package to DEBUG springdoc: api-docs: @@ -21,7 +24,7 @@ springdoc: spring: profiles: - default: dev + default: dev # default profile if none specified server: port: ${SERVER_PORT:8080} @@ -30,8 +33,11 @@ link: expiration-days: ${LINK_EXPIRATION_DAYS:7} jwt: + secret: ${JWT_SECRET:dev-secret-change-me} ttl-seconds: ${JWT_TTL_SECONDS:3600} + + --- # ===================== DEV (H2 in-memory DB) ===================== spring: diff --git a/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java b/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java index b7cc9a7..5cf5f5f 100644 --- a/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java @@ -7,7 +7,7 @@ import org.decepticons.linkshortener.api.exception.NoSuchUserFoundInTheSystemException; import org.decepticons.linkshortener.api.model.User; import org.decepticons.linkshortener.api.service.LinkService; -import org.decepticons.linkshortener.api.service.UserService; +import org.decepticons.linkshortener.api.service.impl.UserServiceImpl; import org.decepticons.linkshortener.api.v1.controller.LinkCrudController; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -41,7 +41,7 @@ class LinkCrudControllerTest { private LinkService linkService; @Mock - UserService userService; + UserServiceImpl userServiceImpl; @InjectMocks @@ -79,10 +79,8 @@ void testCreateShortLink_Success() { fakeUser.getId() ); - when(userService.getCurrentUser()).thenReturn(fakeUser); - - when(linkService.createLink(urlRequestDto, fakeUser)).thenReturn(fakeResponse); + when(linkService.createLink(urlRequestDto)).thenReturn(fakeResponse); ResponseEntity response = linkController.createLink(urlRequestDto); @@ -93,37 +91,34 @@ void testCreateShortLink_Success() { assertEquals("https://example.com/some/long/url", response.getBody().originalUrl()); - verify(userService).getCurrentUser(); - verify(linkService).createLink(urlRequestDto, fakeUser); + + verify(linkService).createLink(urlRequestDto); } @Test @DisplayName("Create Short Link - User Not Found") - void testCreateShortLink_UserNotFound() { - + void testCreateShortLink_UserNotFound_Controller() { UrlRequestDto urlRequestDto = new UrlRequestDto(); urlRequestDto.setUrl("https://example.com/some/long/url"); - when(userService.getCurrentUser()) - .thenThrow(new NoSuchUserFoundInTheSystemException("No such user", "someName")); - + when(linkService.createLink(Mockito.any(UrlRequestDto.class))) + .thenThrow(new NoSuchUserFoundInTheSystemException("User not found", "some-user-id")); - Exception ex = null; - try { - linkController.createLink(urlRequestDto); - }catch (Exception exception){ - ex = exception; - } + NoSuchUserFoundInTheSystemException ex = assertThrows( + NoSuchUserFoundInTheSystemException.class, + () -> linkController.createLink(urlRequestDto) + ); - assertNotNull(ex); - assertInstanceOf(NoSuchUserFoundInTheSystemException.class, ex); + assertEquals("User not found", ex.getMessage()); - verify(linkService, Mockito.never()).createLink(Mockito.any(), Mockito.any()); + verify(linkService).createLink(urlRequestDto); } + + @Test @DisplayName("Get All My Links - Success") void getAllMyLinks_SUCCESS() { @@ -222,8 +217,11 @@ void getAllMyActiveLinks_SUCCESS() { void testDeleteLink_SUCCESS() { UUID linkId = UUID.randomUUID(); + String mockCode = "abc123"; + + ResponseEntity mockResponse = ResponseEntity.noContent().build(); - doNothing().when(linkService).deleteLink(linkId); + when(linkService.deleteLink(linkId)).thenReturn(mockCode); ResponseEntity response = linkController.deleteLink(linkId); diff --git a/src/test/java/org/decepticons/linkshortener/api/controller/RedirectControllerTest.java b/src/test/java/org/decepticons/linkshortener/api/controller/RedirectControllerTest.java index 0bf9642..122da62 100644 --- a/src/test/java/org/decepticons/linkshortener/api/controller/RedirectControllerTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/controller/RedirectControllerTest.java @@ -4,12 +4,11 @@ import jakarta.servlet.http.HttpServletResponse; import org.decepticons.linkshortener.api.dto.LinkResponseDto; import org.decepticons.linkshortener.api.model.User; -import org.decepticons.linkshortener.api.service.LinkService; +import org.decepticons.linkshortener.api.service.impl.LinkServiceImpl; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.Mockito; import java.io.IOException; @@ -24,7 +23,7 @@ class RedirectControllerTest { @InjectMocks private RedirectController redirectController; @Mock - private LinkService linkService; + private LinkServiceImpl linkServiceImpl; @Mock private HttpServletResponse httpServletResponse; @@ -46,16 +45,16 @@ void verifyRedirectToOriginalUrl_SUCCESS() throws IOException { ); - when(linkService.getLinkByCode(code)).thenReturn(responseDto); - when(linkService.incrementClicks(responseDto)).thenReturn(responseDto); - when(linkService.validateLink(responseDto)).thenReturn(true); + when(linkServiceImpl.getLinkByCode(code)).thenReturn(responseDto); + when(linkServiceImpl.incrementClicks(responseDto)).thenReturn(responseDto); + when(linkServiceImpl.validateLink(responseDto)).thenReturn(true); doNothing().when(httpServletResponse).sendRedirect(responseDto.originalUrl()); redirectController.redirect(code, httpServletResponse); verify(httpServletResponse, times(1)).sendRedirect("https://www.example.com"); - verify(linkService, times(1)).incrementClicks(responseDto); + verify(linkServiceImpl, times(1)).incrementClicks(responseDto); } @Test @@ -75,8 +74,8 @@ void verifyExceptionThrownWhenLinkNotValid_SUCCESS() throws IOException { owner.getId() ); - when(linkService.getLinkByCode(code)).thenReturn(responseDto); - when(linkService.validateLink(responseDto)).thenReturn(false ); + when(linkServiceImpl.getLinkByCode(code)).thenReturn(responseDto); + when(linkServiceImpl.validateLink(responseDto)).thenReturn(false ); Exception ex = null; diff --git a/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceMethodsTest.java b/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplMethodsTest.java similarity index 87% rename from src/test/java/org/decepticons/linkshortener/api/service/LinkServiceMethodsTest.java rename to src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplMethodsTest.java index 6425963..86e0e1f 100644 --- a/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceMethodsTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplMethodsTest.java @@ -5,7 +5,8 @@ import org.decepticons.linkshortener.api.model.LinkStatus; import org.decepticons.linkshortener.api.model.User; import org.decepticons.linkshortener.api.repository.LinkRepository; -import org.decepticons.linkshortener.api.repository.UserRepository; +import org.decepticons.linkshortener.api.service.impl.LinkServiceImpl; +import org.decepticons.linkshortener.api.service.impl.UserServiceImpl; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -30,18 +31,16 @@ @ExtendWith(MockitoExtension.class) -public class LinkServiceMethodsTest { +public class LinkServiceImplMethodsTest { @Mock private LinkRepository linkRepository; @Mock - private UserService userService; + private UserServiceImpl userServiceImpl; - @Mock - private CacheEvictService cacheEvictService; @InjectMocks - private LinkService linkService; + private LinkServiceImpl linkService; private final UUID testUserId = UUID.randomUUID(); private final String testUsername = "testuser"; @@ -73,7 +72,7 @@ void getAllMyLinks_SUCCESS() { Page mockPage = new PageImpl<>(List.of(link1)); - when(userService.getCurrentUserId()).thenReturn(testUserId); + when(userServiceImpl.getCurrentUserId()).thenReturn(testUserId); when(linkRepository.findAllByOwnerId(eq(testUserId), any(Pageable.class))) .thenReturn(mockPage); @@ -93,7 +92,7 @@ void getAllMyActiveLinks_SUCCESS() { Page mockPage = new PageImpl<>(List.of(link1)); - when(userService.getCurrentUserId()).thenReturn(testUserId); + when(userServiceImpl.getCurrentUserId()).thenReturn(testUserId); when(linkRepository.findAllByOwnerIdAndStatus(eq(testUserId), eq(LinkStatus.ACTIVE), any(Pageable.class))) .thenReturn(mockPage); @@ -115,13 +114,12 @@ void deleteLink_SUCCESS() { link.setCode("abc123"); link.setOwner(owner); - when(userService.getCurrentUserId()).thenReturn(testUserId); + when(userServiceImpl.getCurrentUserId()).thenReturn(testUserId); when(linkRepository.findById(linkId)).thenReturn(Optional.of(link)); linkService.deleteLink(linkId); verify(linkRepository, times(1)).delete(link); - verify(cacheEvictService, times(1)).evictLink("abc123"); } diff --git a/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceTest.java b/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplTest.java similarity index 84% rename from src/test/java/org/decepticons/linkshortener/api/service/LinkServiceTest.java rename to src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplTest.java index 41a6f61..7c54024 100644 --- a/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplTest.java @@ -6,15 +6,14 @@ import org.decepticons.linkshortener.api.model.LinkStatus; import org.decepticons.linkshortener.api.model.User; import org.decepticons.linkshortener.api.repository.LinkRepository; -import org.decepticons.linkshortener.api.repository.UserRepository; +import org.decepticons.linkshortener.api.service.impl.LinkServiceImpl; +import org.decepticons.linkshortener.api.service.impl.UserServiceImpl; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; @@ -22,7 +21,6 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.util.List; import java.util.Optional; import java.util.UUID; @@ -32,16 +30,16 @@ import static org.mockito.Mockito.*; @ExtendWith(org.mockito.junit.jupiter.MockitoExtension.class) -class LinkServiceTest { +class LinkServiceImplTest { @Mock private LinkRepository linkRepository; @Mock - private UserService userService; + private UserServiceImpl userServiceImpl; @InjectMocks - private LinkService linkService; + private LinkServiceImpl linkServiceImpl; @@ -59,10 +57,10 @@ void testLinkCreation_Success() { urlRequestDto.setUrl("https://www.example.com"); - + when(userServiceImpl.getCurrentUser()).thenReturn(user); when(linkRepository.save(any(Link.class))).thenAnswer(i -> i.getArguments()[0]); - LinkResponseDto result = linkService.createLink(urlRequestDto, user); + LinkResponseDto result = linkServiceImpl.createLink(urlRequestDto); assertEquals(urlRequestDto.getUrl(), result.originalUrl()); assertEquals(user.getId(), result.ownerId()); @@ -88,13 +86,11 @@ void incrementOfClicks_Success() { link.setCode("abc123"); link.setClicks(0); + when(linkRepository.incrementClicksByCodeNative("abc123")).thenReturn(1); when(linkRepository.findByCode("abc123")).thenReturn(Optional.of(link)); - when(linkRepository.save(any(Link.class))).thenAnswer(i -> i.getArguments()[0]); - - - linkService.incrementClicks(linkResponseDto); + LinkResponseDto result = linkServiceImpl.incrementClicks(linkResponseDto); + assertNotEquals(1, result.clicks()); - assertNotEquals(0, link.getClicks()); } @Test @@ -112,7 +108,7 @@ void testMapToResponse_Success() { link.setExpiresAt(Instant.now().plus(2, ChronoUnit.DAYS)); link.setOwner(owner); - LinkResponseDto dto = linkService.mapToResponse(link); + LinkResponseDto dto = linkServiceImpl.mapToResponse(link); assertEquals(link.getCode(), dto.code()); assertEquals(link.getOriginalUrl(), dto.originalUrl()); @@ -142,7 +138,7 @@ void getLinkByCode_Success() { when(linkRepository.findByCode("abc123")).thenReturn(Optional.of(link)); - LinkResponseDto result = linkService.getLinkByCode("abc123"); + LinkResponseDto result = linkServiceImpl.getLinkByCode("abc123"); assertEquals("abc123", result.code()); assertEquals("https://example.com", result.originalUrl()); @@ -178,7 +174,7 @@ void deactivateLink_Success() { when(linkRepository.findByCode("abc123")).thenReturn(Optional.of(link)); when(linkRepository.save(any(Link.class))).thenAnswer(i -> i.getArguments()[0]); - LinkResponseDto result = linkService.deactivateLink(linkResponseDto); + LinkResponseDto result = linkServiceImpl.deactivateLink(linkResponseDto); assertEquals("INACTIVE", result.status()); } @@ -198,8 +194,8 @@ void validateLink_Success() { UUID.randomUUID() ); - linkService.validateLink(linkResponseDto); - Assertions.assertTrue(linkService.validateLink(linkResponseDto)); + linkServiceImpl.validateLink(linkResponseDto); + Assertions.assertTrue(linkServiceImpl.validateLink(linkResponseDto)); } @@ -223,9 +219,9 @@ void testUpdateLinkExpiration(){ when(linkRepository.findByCode(code)).thenReturn(Optional.of(link)); when(linkRepository.save(any(Link.class))).thenAnswer(i -> i.getArguments()[0]); - when(userService.getCurrentUserId()).thenReturn(owner.getId()); + when(userServiceImpl.getCurrentUserId()).thenReturn(owner.getId()); - LinkResponseDto response = linkService.updateLinkExpiration(code, Instant + LinkResponseDto response = linkServiceImpl.updateLinkExpiration(code, Instant .now() .plusSeconds(7200)); diff --git a/src/test/java/org/decepticons/linkshortener/api/service/UserServiceImplTest.java b/src/test/java/org/decepticons/linkshortener/api/service/UserServiceImplTest.java new file mode 100644 index 0000000..74bbba0 --- /dev/null +++ b/src/test/java/org/decepticons/linkshortener/api/service/UserServiceImplTest.java @@ -0,0 +1,98 @@ +package org.decepticons.linkshortener.api.service; + +import org.decepticons.linkshortener.api.exception.NoSuchUserFoundInTheSystemException; +import org.decepticons.linkshortener.api.model.User; +import org.decepticons.linkshortener.api.repository.UserRepository; +import org.decepticons.linkshortener.api.service.impl.UserServiceImpl; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class UserServiceImplTest { + @Mock + private UserRepository userRepository; + + @InjectMocks + private UserServiceImpl userService; + + @Mock + private SecurityContext securityContext; + + @Mock + private Authentication authentication; + + @BeforeEach + void setUp() { + when(authentication.getName()).thenReturn("testuser"); + when(securityContext.getAuthentication()).thenReturn(authentication); + + + org.springframework.security.core.context.SecurityContextHolder.setContext(securityContext); + } + + @AfterEach + void tearDown() { + org.springframework.security.core.context.SecurityContextHolder.clearContext(); + } + + @Test + @DisplayName("Test getCurrentUser returns correct User") + void testGetCurrentUser_Success() { + User fakeUser = new User(); + fakeUser.setId(UUID.randomUUID()); + fakeUser.setUsername("testuser"); + + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(fakeUser)); + + User result = userService.getCurrentUser(); + + assertNotNull(result); + assertEquals("testuser", result.getUsername()); + } + + @Test + @DisplayName("Test getCurrentUserId returns correct UUID") + void testGetCurrentUserId_Success() { + UUID fakeId = UUID.randomUUID(); + User fakeUser = new User(); + fakeUser.setId(fakeId); + fakeUser.setUsername("testuser"); + + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(fakeUser)); + + UUID result = userService.getCurrentUserId(); + + assertEquals(fakeId, result); + } + + @Test + @DisplayName("Test getCurrentUser throws exception when user not found") + void testGetCurrentUser_UserNotFound() { + when(userRepository.findByUsername("testuser")).thenReturn(Optional.empty()); + + NoSuchUserFoundInTheSystemException ex = assertThrows( + NoSuchUserFoundInTheSystemException.class, + () -> userService.getCurrentUser() + ); + + assertTrue(ex.getMessage().contains("No such user found in the system")); + } + +} diff --git a/src/test/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudControllerTest.java b/src/test/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudControllerTest.java index 45e2747..957385f 100644 --- a/src/test/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudControllerTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudControllerTest.java @@ -1,9 +1,8 @@ package org.decepticons.linkshortener.api.v1.controller; import org.decepticons.linkshortener.api.dto.LinkResponseDto; -import org.decepticons.linkshortener.api.service.LinkService; +import org.decepticons.linkshortener.api.service.impl.LinkServiceImpl; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; @@ -11,6 +10,7 @@ import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; @@ -20,6 +20,7 @@ import java.util.UUID; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -31,7 +32,7 @@ class LinkCrudControllerTest { private MockMvc mockMvc; @MockitoBean - private LinkService linkService; + private LinkServiceImpl linkServiceImpl; @Test @WithMockUser(username = "testuser", roles = {"USER"}) @@ -53,7 +54,7 @@ void testGetAllMyLinks() throws Exception { 1 ); - Mockito.when(linkService.getAllMyLinks(anyInt(), anyInt())).thenReturn(page); + when(linkServiceImpl.getAllMyLinks(anyInt(), anyInt())).thenReturn(page); mockMvc.perform(get("/api/v1/links/my_all_links") .param("page", "0") @@ -85,7 +86,7 @@ void testGetAllMyActiveLinks() throws Exception { 1 ); - Mockito.when(linkService.getAllMyActiveLinks(anyInt(), anyInt())).thenReturn(page); + when(linkServiceImpl.getAllMyActiveLinks(anyInt(), anyInt())).thenReturn(page); mockMvc.perform(get("/api/v1/links/my_all_active_links") .param("page", "0") @@ -100,12 +101,17 @@ void testGetAllMyActiveLinks() throws Exception { @Test @WithMockUser(username = "testuser", roles = {"USER"}) void testDeleteLinkSuccess() throws Exception { + ResponseEntity responseEntity = ResponseEntity.noContent().build(); UUID existingId = UUID.randomUUID(); - Mockito.doNothing().when(linkService).deleteLink(existingId); + + when(linkServiceImpl.deleteLink(existingId)) + .thenReturn(existingId.toString()); mockMvc.perform(delete("/api/v1/links/delete/{id}", existingId.toString())) .andExpect(status().isNoContent()); + + verify(linkServiceImpl, times(1)).deleteLink(existingId); } } From a22ee4b544a94084cdc03a58a60cb0bd5e79a2d3 Mon Sep 17 00:00:00 2001 From: zkhr Date: Mon, 15 Sep 2025 20:30:47 +0200 Subject: [PATCH 07/11] fixed logical problems --- .../api/service/impl/LinkServiceImpl.java | 295 ++++++++++++++++++ .../api/service/impl/UserServiceImpl.java | 53 ++++ .../api/controller/CacheControllerTest.java | 30 -- 3 files changed, 348 insertions(+), 30 deletions(-) create mode 100644 src/main/java/org/decepticons/linkshortener/api/service/impl/LinkServiceImpl.java create mode 100644 src/main/java/org/decepticons/linkshortener/api/service/impl/UserServiceImpl.java delete mode 100644 src/test/java/org/decepticons/linkshortener/api/controller/CacheControllerTest.java diff --git a/src/main/java/org/decepticons/linkshortener/api/service/impl/LinkServiceImpl.java b/src/main/java/org/decepticons/linkshortener/api/service/impl/LinkServiceImpl.java new file mode 100644 index 0000000..7d200e9 --- /dev/null +++ b/src/main/java/org/decepticons/linkshortener/api/service/impl/LinkServiceImpl.java @@ -0,0 +1,295 @@ +package org.decepticons.linkshortener.api.service.impl; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Random; +import java.util.UUID; +import org.decepticons.linkshortener.api.dto.LinkResponseDto; +import org.decepticons.linkshortener.api.dto.UrlRequestDto; +import org.decepticons.linkshortener.api.exception.InvalidExpirationDateException; +import org.decepticons.linkshortener.api.exception.NoSuchShortLinkFoundInTheSystemException; +import org.decepticons.linkshortener.api.model.Link; +import org.decepticons.linkshortener.api.model.LinkStatus; +import org.decepticons.linkshortener.api.repository.LinkRepository; +import org.decepticons.linkshortener.api.service.LinkService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Application service responsible for creating and maintaining {@link Link} entities. + * Provides operations for link creation, click tracking, simple DTO mapping, + * and basic liveness checks (active + not expired). + *

+ */ + + +@Service +public class LinkServiceImpl implements LinkService { + + @Value("${link.expiration-days}") + private long linkExpirationDays; + + private final LinkRepository linkRepository; + private final UserServiceImpl userServiceImpl; + private final Random random = new Random(); + + + /** + * Creates a new {@code LinkService}. + * + * @param linkRepository repository used to persist and load {@link Link} entities + */ + + public LinkServiceImpl(LinkRepository linkRepository, + UserServiceImpl userServiceImpl) { + this.linkRepository = linkRepository; + this.userServiceImpl = userServiceImpl; + } + + + /** + * Creates and persists a new {@link Link}. + * + * @param originalUrl the original long URL to be shortened + * @return a {@link LinkResponseDto} representing the newly created link + */ + @Override + @Transactional + public LinkResponseDto createLink(UrlRequestDto originalUrl) { + Link link = new Link(); + link.setOriginalUrl(originalUrl.getUrl()); + link.setOwner(userServiceImpl.getCurrentUser()); + + link.setCode(generateRandomCode()); + link.setExpiresAt(Instant.now().plus(linkExpirationDays, ChronoUnit.DAYS)); + link.setStatus(LinkStatus.ACTIVE); + + Link saved = linkRepository.save(link); + + return mapToResponse(saved); + } + + + /** + * Increments the click counter of the given link and persists the change. + * This should be invoked whenever the shortened URL is accessed. + * Internally, the entity updates its {@code lastAccessedAt} timestamp. + *

+ * + * @param link the link whose click counter should be incremented + */ + @Override + @Transactional + public LinkResponseDto incrementClicks(LinkResponseDto link) { + + int affectedRows = linkRepository.incrementClicksByCodeNative(link.code()); + + if (affectedRows == 0) { + throw new NoSuchShortLinkFoundInTheSystemException( + "No such short link found in the system: " + link.code(), + link.code() + ); + } + Link updatedLink = linkRepository.findByCode(link.code()) + .orElseThrow(() -> new NoSuchShortLinkFoundInTheSystemException( + "No such short link found in the system: " + link.code(), + link.code() + )); + + return mapToResponse(updatedLink); + } + + + /** + * Maps a {@link Link} JPA entity to a transport-friendly {@link LinkResponseDto}. + * + * @param link the entity to map + * @return a response DTO with the most relevant fields + */ + public LinkResponseDto mapToResponse(Link link) { + return new LinkResponseDto( + + link.getId(), + link.getCode(), + link.getOriginalUrl(), + link.getCreatedAt(), + link.getExpiresAt(), + link.getClicks(), + link.getStatus().name(), + link.getOwner().getId() + ); + } + + + /** + * Generates a pseudo-random short code of fixed length . + * + * @return a new short code (e.g., {@code "aZ3fQ1"}) + */ + private String generateRandomCode() { + String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 6; i++) { + sb.append(chars.charAt(random.nextInt(chars.length()))); + } + return sb.toString(); + } + + + /** + * Retrieves a {@link Link} entity by its short code. + * + * @param code short link code. + * @return Optional + */ + @Override + @Cacheable(value = "shortLinksCache", key = "#code") + public LinkResponseDto getLinkByCode(String code) { + Link link = linkRepository.findByCode(code) + .orElseThrow(() -> new NoSuchShortLinkFoundInTheSystemException( + "No such short link found in the system: " + code, + code + )); + + return mapToResponse(link); + + } + + + /** + * Deactivates a link by setting its status to INACTIVE. + * + * @param link the link to deactivate + * @return the updated {@link LinkResponseDto} with status set to INACTIVE + */ + + @Override + @CacheEvict(value = "shortLinksCache", key = "#result.code") + public LinkResponseDto deactivateLink(LinkResponseDto link) { + Link linkByCode = linkRepository.findByCode(link.code()) + .orElseThrow(() -> new NoSuchShortLinkFoundInTheSystemException( + "No such short link found in the system: " + link.code(), + link.code() + )); + linkByCode.setStatus(LinkStatus.INACTIVE); + Link saved = linkRepository.save(linkByCode); + return mapToResponse(saved); + } + + + /** + * Validates if a link is active and not expired. + * + * @param link the link to validate + * @return {@code true} if the link is active and not expired; {@code false} otherwise + */ + @Override + public boolean validateLink(LinkResponseDto link) { + return link.status().equalsIgnoreCase(LinkStatus.ACTIVE.toString()) + && (link.expiresAt() == null || link.expiresAt().isAfter(Instant.now())); + } + + + /** + * Retrieves all links of the currently authenticated user with pagination. + * + * @param page the page number to retrieve (0-based) + * @param size the number of records per page + * @return a {@link Page} of {@link LinkResponseDto} objects representing all user's links + */ + @Override + public Page getAllMyLinks(int page, int size) { + UUID userId = userServiceImpl.getCurrentUserId(); + Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending()); + return linkRepository.findAllByOwnerId(userId, pageable) + .map(this::mapToResponse); + } + + /** + * Retrieves all active links of the currently authenticated user with pagination. + * + * @param page the page number to retrieve (0-based) + * @param size the number of records per page + * @return a {@link Page} of {@link LinkResponseDto} objects representing active user's links + */ + @Override + public Page getAllMyActiveLinks(int page, int size) { + UUID userId = userServiceImpl.getCurrentUserId(); + Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending()); + return linkRepository.findAllByOwnerIdAndStatus(userId, LinkStatus.ACTIVE, pageable) + .map(this::mapToResponse); + } + + /** + * Deletes a link from the database if it belongs to the currently authenticated user. + * + * @param linkId the unique identifier of the link to delete + */ + @Transactional + @Override + @CacheEvict(value = "shortLinksCache", key = "#result") + public String deleteLink(UUID linkId) { + UUID currentUserId = userServiceImpl.getCurrentUserId(); + + Link link = linkRepository.findById(linkId) + .orElseThrow(() -> new NoSuchShortLinkFoundInTheSystemException( + "No such short link found in the system", linkId.toString() + )); + + + if (!link.getOwner().getId().equals(currentUserId)) { + throw new AccessDeniedException("You are not allowed to delete this link"); + } + + linkRepository.delete(link); + + return link.getCode(); + } + + /** + * Retrieves the UUID of the currently authenticated user from the security context. + * + * @return the UUID of the authenticated user + */ + + + + + @CachePut(value = "shortLinksCache", key = "#code") + @Override + public LinkResponseDto updateLinkExpiration(String code, Instant newExpirationDate) { + Link link = linkRepository.findByCode(code) + .orElseThrow(() -> new NoSuchShortLinkFoundInTheSystemException( + "No such short link found in the system: " + code, + code + )); + + if (!link.getOwner().getId().equals(userServiceImpl.getCurrentUserId())) { + throw new AccessDeniedException("You are not allowed to update this link"); + } + + if (newExpirationDate.isBefore(Instant.now())) { + throw new InvalidExpirationDateException( + "Expiration date must be in the future", + newExpirationDate + ); + } + + link.setExpiresAt(newExpirationDate); + Link saved = linkRepository.save(link); + + return mapToResponse(saved); + } + + +} + diff --git a/src/main/java/org/decepticons/linkshortener/api/service/impl/UserServiceImpl.java b/src/main/java/org/decepticons/linkshortener/api/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..e2ea2f1 --- /dev/null +++ b/src/main/java/org/decepticons/linkshortener/api/service/impl/UserServiceImpl.java @@ -0,0 +1,53 @@ +package org.decepticons.linkshortener.api.service.impl; + +import java.util.UUID; +import org.decepticons.linkshortener.api.exception.NoSuchUserFoundInTheSystemException; +import org.decepticons.linkshortener.api.model.User; +import org.decepticons.linkshortener.api.repository.UserRepository; +import org.decepticons.linkshortener.api.service.UserService; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + + +/** + * Service implementation for retrieving information about the currently authenticated user. + */ +@Service +public class UserServiceImpl implements UserService { + + public UserRepository userRepository; + + /** + * Constructs a new UserServiceImpl with the given UserRepository. + * + * @param userRepository the repository used to access user data + */ + public UserServiceImpl(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public UUID getCurrentUserId() { + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new NoSuchUserFoundInTheSystemException( + "No such user found in the system: " + username, + username + )); + return user.getId(); + + } + + @Override + public User getCurrentUser() { + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + return userRepository.findByUsername(username) + .orElseThrow(() -> new NoSuchUserFoundInTheSystemException( + "No such user found in the system: " + username, + username + )); + } + + + +} diff --git a/src/test/java/org/decepticons/linkshortener/api/controller/CacheControllerTest.java b/src/test/java/org/decepticons/linkshortener/api/controller/CacheControllerTest.java deleted file mode 100644 index e92a970..0000000 --- a/src/test/java/org/decepticons/linkshortener/api/controller/CacheControllerTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.decepticons.linkshortener.api.controller; - -import org.decepticons.linkshortener.api.service.CacheInspectionService; -import org.decepticons.linkshortener.api.v1.controller.CacheController; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; - -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -@ExtendWith(org.mockito.junit.jupiter.MockitoExtension.class) - class CacheControllerTest { - @Mock - private CacheInspectionService cacheInspectionService; - - @InjectMocks - private CacheController cacheController; - - @Test - @DisplayName("Inspect Cache - Service Called") - void inspectCache_SUCCESS() { - - cacheController.inspectCache(); - - verify(cacheInspectionService, times(1)).printCache("shortLinksCache"); - } -} From 222d2859ab5610a8d4cc518ddcebbc5af388a113 Mon Sep 17 00:00:00 2001 From: zkhr Date: Mon, 15 Sep 2025 21:19:47 +0200 Subject: [PATCH 08/11] logical update --- .../api/security/service/impl/AuthServiceImpl.java | 2 +- .../api/security/service/impl/UserAuthServiceImpl.java | 6 +++--- src/main/resources/application.yaml | 6 ------ .../api/exceptions/GlobalExceptionHandlerTest.java | 8 ++++---- .../api/security/service/impl/AuthServiceImplTest.java | 5 +++-- .../security/service/impl/UserAuthServiceImplTest.java | 7 ++++--- 6 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/decepticons/linkshortener/api/security/service/impl/AuthServiceImpl.java b/src/main/java/org/decepticons/linkshortener/api/security/service/impl/AuthServiceImpl.java index 40548bc..4fae884 100644 --- a/src/main/java/org/decepticons/linkshortener/api/security/service/impl/AuthServiceImpl.java +++ b/src/main/java/org/decepticons/linkshortener/api/security/service/impl/AuthServiceImpl.java @@ -3,7 +3,7 @@ import java.util.Collections; import java.util.Optional; import lombok.RequiredArgsConstructor; -import org.decepticons.linkshortener.api.exceptions.InvalidTokenException; +import org.decepticons.linkshortener.api.exception.InvalidTokenException; import org.decepticons.linkshortener.api.model.Role; import org.decepticons.linkshortener.api.model.User; import org.decepticons.linkshortener.api.model.UserStatus; diff --git a/src/main/java/org/decepticons/linkshortener/api/security/service/impl/UserAuthServiceImpl.java b/src/main/java/org/decepticons/linkshortener/api/security/service/impl/UserAuthServiceImpl.java index 62005e5..fb448d9 100644 --- a/src/main/java/org/decepticons/linkshortener/api/security/service/impl/UserAuthServiceImpl.java +++ b/src/main/java/org/decepticons/linkshortener/api/security/service/impl/UserAuthServiceImpl.java @@ -2,9 +2,9 @@ import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; -import org.decepticons.linkshortener.api.exceptions.InvalidPasswordException; -import org.decepticons.linkshortener.api.exceptions.UserAlreadyExistsException; -import org.decepticons.linkshortener.api.exceptions.UserNotFoundException; +import org.decepticons.linkshortener.api.exception.InvalidPasswordException; +import org.decepticons.linkshortener.api.exception.UserAlreadyExistsException; +import org.decepticons.linkshortener.api.exception.UserNotFoundException; import org.decepticons.linkshortener.api.model.User; import org.decepticons.linkshortener.api.repository.UserRepository; import org.decepticons.linkshortener.api.security.service.UserAuthService; diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 0593788..8947d45 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -8,12 +8,6 @@ # ==================================================== logging: - level: - org.springdoc: DEBUG - root: DEBUG # set global logging to DEBUG - org.springframework: INFO # optional: keep Spring framework logs at INFO to reduce noise - com.yourpackage: DEBUG # set your own package to DEBUG - springdoc: api-docs: path: /v3/api-docs diff --git a/src/test/java/org/decepticons/linkshortener/api/exceptions/GlobalExceptionHandlerTest.java b/src/test/java/org/decepticons/linkshortener/api/exceptions/GlobalExceptionHandlerTest.java index 9c8bdab..925b84c 100644 --- a/src/test/java/org/decepticons/linkshortener/api/exceptions/GlobalExceptionHandlerTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/exceptions/GlobalExceptionHandlerTest.java @@ -1,11 +1,13 @@ package org.decepticons.linkshortener.api.exceptions; +import io.jsonwebtoken.JwtException; import org.decepticons.linkshortener.api.controller.GlobalExceptionHandlerController; import org.decepticons.linkshortener.api.exception.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.BadCredentialsException; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.bind.annotation.GetMapping; @@ -135,10 +137,8 @@ void givenGenericException_whenThrown_thenReturnInternalServerError() throws Exc */ @RestController private static class TestController { - @GetMapping("/test-user-exists") - public void testUserExists() { - throw new UserAlreadyExistsException("testuser"); - } + + @GetMapping("/test-authentication-exception") public void testAuthenticationException() { throw new BadCredentialsException("Invalid username or password"); diff --git a/src/test/java/org/decepticons/linkshortener/api/security/service/impl/AuthServiceImplTest.java b/src/test/java/org/decepticons/linkshortener/api/security/service/impl/AuthServiceImplTest.java index f444def..d7204fa 100644 --- a/src/test/java/org/decepticons/linkshortener/api/security/service/impl/AuthServiceImplTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/security/service/impl/AuthServiceImplTest.java @@ -1,7 +1,8 @@ package org.decepticons.linkshortener.api.security.service.impl; -import org.decepticons.linkshortener.api.exceptions.InvalidTokenException; -import org.decepticons.linkshortener.api.exceptions.UserAlreadyExistsException; + +import org.decepticons.linkshortener.api.exception.InvalidTokenException; +import org.decepticons.linkshortener.api.exception.UserAlreadyExistsException; import org.decepticons.linkshortener.api.model.User; import org.decepticons.linkshortener.api.model.Role; import org.decepticons.linkshortener.api.model.UserStatus; diff --git a/src/test/java/org/decepticons/linkshortener/api/security/service/impl/UserAuthServiceImplTest.java b/src/test/java/org/decepticons/linkshortener/api/security/service/impl/UserAuthServiceImplTest.java index d427044..0340dd3 100644 --- a/src/test/java/org/decepticons/linkshortener/api/security/service/impl/UserAuthServiceImplTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/security/service/impl/UserAuthServiceImplTest.java @@ -1,8 +1,9 @@ package org.decepticons.linkshortener.api.security.service.impl; -import org.decepticons.linkshortener.api.exceptions.InvalidPasswordException; -import org.decepticons.linkshortener.api.exceptions.UserAlreadyExistsException; -import org.decepticons.linkshortener.api.exceptions.UserNotFoundException; + +import org.decepticons.linkshortener.api.exception.InvalidPasswordException; +import org.decepticons.linkshortener.api.exception.UserAlreadyExistsException; +import org.decepticons.linkshortener.api.exception.UserNotFoundException; import org.decepticons.linkshortener.api.model.User; import org.decepticons.linkshortener.api.repository.UserRepository; import org.junit.jupiter.api.BeforeEach; From 998445b4338d8a0cc572722201201db7ef8cd0f8 Mon Sep 17 00:00:00 2001 From: zkhr Date: Mon, 15 Sep 2025 21:49:39 +0200 Subject: [PATCH 09/11] removed setter --- .../org/decepticons/linkshortener/api/model/User.java | 2 +- .../api/controller/LinkCrudControllerTest.java | 2 +- .../api/controller/RedirectControllerTest.java | 4 ++-- .../api/service/LinkServiceImplMethodsTest.java | 4 ++-- .../linkshortener/api/service/LinkServiceImplTest.java | 8 ++++---- .../linkshortener/api/service/UserServiceImplTest.java | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/decepticons/linkshortener/api/model/User.java b/src/main/java/org/decepticons/linkshortener/api/model/User.java index afc30b0..c904221 100644 --- a/src/main/java/org/decepticons/linkshortener/api/model/User.java +++ b/src/main/java/org/decepticons/linkshortener/api/model/User.java @@ -52,7 +52,7 @@ public class User { @Id @GeneratedValue - @Setter +// @Setter @Column(name = "id", nullable = false, updatable = false) private UUID id; diff --git a/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java b/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java index 5cf5f5f..af33ade 100644 --- a/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java @@ -64,7 +64,7 @@ void testCreateShortLink_Success() { urlRequestDto.setUrl("https://example.com/some/long/url"); User fakeUser = new User(); - fakeUser.setId(UUID.randomUUID()); +// fakeUser.setId(UUID.randomUUID()); fakeUser.setUsername("someName"); diff --git a/src/test/java/org/decepticons/linkshortener/api/controller/RedirectControllerTest.java b/src/test/java/org/decepticons/linkshortener/api/controller/RedirectControllerTest.java index 122da62..720d60f 100644 --- a/src/test/java/org/decepticons/linkshortener/api/controller/RedirectControllerTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/controller/RedirectControllerTest.java @@ -31,7 +31,7 @@ class RedirectControllerTest { void verifyRedirectToOriginalUrl_SUCCESS() throws IOException { String code = "abc123"; User owner = new User(); - owner.setId(java.util.UUID.randomUUID()); +// owner.setId(java.util.UUID.randomUUID()); LinkResponseDto responseDto = new LinkResponseDto( java.util.UUID.randomUUID(), @@ -61,7 +61,7 @@ void verifyRedirectToOriginalUrl_SUCCESS() throws IOException { void verifyExceptionThrownWhenLinkNotValid_SUCCESS() throws IOException { String code = "abc123"; User owner = new User(); - owner.setId(java.util.UUID.randomUUID()); +// owner.setId(java.util.UUID.randomUUID()); LinkResponseDto responseDto = new LinkResponseDto( java.util.UUID.randomUUID(), diff --git a/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplMethodsTest.java b/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplMethodsTest.java index 86e0e1f..958cf84 100644 --- a/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplMethodsTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplMethodsTest.java @@ -54,7 +54,7 @@ void setUpSecurityContext() { User testUser = new User(); - testUser.setId(testUserId); +// testUser.setId(testUserId); testUser.setUsername(testUsername); } @@ -108,7 +108,7 @@ void getAllMyActiveLinks_SUCCESS() { void deleteLink_SUCCESS() { UUID linkId = UUID.randomUUID(); User owner = new User(); - owner.setId(testUserId); +// owner.setId(testUserId); owner.setUsername(testUsername); Link link = new Link(); link.setCode("abc123"); diff --git a/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplTest.java b/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplTest.java index 7c54024..d7d2de2 100644 --- a/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplTest.java @@ -52,7 +52,7 @@ class LinkServiceImplTest { void testLinkCreation_Success() { UrlRequestDto urlRequestDto = new UrlRequestDto(); User user = new User(); - user.setId(UUID.randomUUID()); +// user.setId(UUID.randomUUID()); urlRequestDto.setUrl("https://www.example.com"); @@ -98,7 +98,7 @@ void incrementOfClicks_Success() { void testMapToResponse_Success() { UUID userId = UUID.randomUUID(); User owner = new User(); - owner.setId(userId); +// owner.setId(userId); Link link = new Link(); link.setCode("abc123"); @@ -125,7 +125,7 @@ void testMapToResponse_Success() { void getLinkByCode_Success() { User owner = new User(); - owner.setId(UUID.randomUUID()); +// owner.setId(UUID.randomUUID()); Link link = new Link(); link.setOwner(owner); @@ -205,7 +205,7 @@ void testUpdateLinkExpiration(){ String code = "abc123"; User owner = new User(); owner.setUsername("testUser"); - owner.setId(UUID.randomUUID()); +// owner.setId(UUID.randomUUID()); Link link = new Link(); link.setCode(code); diff --git a/src/test/java/org/decepticons/linkshortener/api/service/UserServiceImplTest.java b/src/test/java/org/decepticons/linkshortener/api/service/UserServiceImplTest.java index 74bbba0..c0f47be 100644 --- a/src/test/java/org/decepticons/linkshortener/api/service/UserServiceImplTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/service/UserServiceImplTest.java @@ -56,7 +56,7 @@ void tearDown() { @DisplayName("Test getCurrentUser returns correct User") void testGetCurrentUser_Success() { User fakeUser = new User(); - fakeUser.setId(UUID.randomUUID()); +// fakeUser.setId(UUID.randomUUID()); fakeUser.setUsername("testuser"); when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(fakeUser)); @@ -72,7 +72,7 @@ void testGetCurrentUser_Success() { void testGetCurrentUserId_Success() { UUID fakeId = UUID.randomUUID(); User fakeUser = new User(); - fakeUser.setId(fakeId); +// fakeUser.setId(fakeId); fakeUser.setUsername("testuser"); when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(fakeUser)); From 7ec5918fb7e567984680102762566058715de2a5 Mon Sep 17 00:00:00 2001 From: zkhr Date: Tue, 16 Sep 2025 10:50:20 +0200 Subject: [PATCH 10/11] removed setter --- src/main/java/org/decepticons/linkshortener/api/model/User.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/decepticons/linkshortener/api/model/User.java b/src/main/java/org/decepticons/linkshortener/api/model/User.java index c904221..65ad14b 100644 --- a/src/main/java/org/decepticons/linkshortener/api/model/User.java +++ b/src/main/java/org/decepticons/linkshortener/api/model/User.java @@ -52,7 +52,6 @@ public class User { @Id @GeneratedValue -// @Setter @Column(name = "id", nullable = false, updatable = false) private UUID id; From 1b2feff04d804fe581e454e2bb07d403d9ae6021 Mon Sep 17 00:00:00 2001 From: zkhr Date: Tue, 16 Sep 2025 12:22:00 +0200 Subject: [PATCH 11/11] fixed checkstyle --- .../exceptions/GlobalExceptionHandler.java | 0 .../linkshortener/api/model/Link.java | 8 +- .../linkshortener/api/model/User.java | 4 +- .../api/repository/LinkRepository.java | 16 +- .../GlobalExceptionHandlerController.java | 2 +- .../controller/LinkCrudControllerTest.java | 35 +- .../GlobalExceptionHandlerTest.java | 19 +- .../linkshortener/api/model/RoleTest.java | 5 +- .../controller/AuthControllerTest.java | 331 +++++++++--------- .../jwt/JwtAuthenticationFilterTest.java | 36 +- .../api/security/jwt/JwtTokenUtilTest.java | 14 +- .../security/model/CustomUserDetailsTest.java | 10 +- .../service/impl/AuthServiceImplTest.java | 27 +- .../service/impl/UserAuthServiceImplTest.java | 25 +- .../service/LinkServiceImplMethodsTest.java | 38 +- .../api/service/LinkServiceImplTest.java | 65 ++-- .../api/service/UserServiceImplTest.java | 32 +- .../api/util/AuthMapperTest.java | 15 +- .../api/util/PasswordValidatorTest.java | 6 +- .../v1/controller/CacheControllerTest.java | 254 -------------- .../v1/controller/LinkCrudControllerTest.java | 22 +- .../LinkShortenerApplicationTests.java | 30 +- .../unversioned/HealthControllerTest.java | 29 +- .../unversioned/RedirectControllerTest.java | 30 +- 24 files changed, 446 insertions(+), 607 deletions(-) delete mode 100644 src/main/java/org/decepticons/linkshortener/api/exceptions/GlobalExceptionHandler.java delete mode 100644 src/test/java/org/decepticons/linkshortener/api/v1/controller/CacheControllerTest.java diff --git a/src/main/java/org/decepticons/linkshortener/api/exceptions/GlobalExceptionHandler.java b/src/main/java/org/decepticons/linkshortener/api/exceptions/GlobalExceptionHandler.java deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/org/decepticons/linkshortener/api/model/Link.java b/src/main/java/org/decepticons/linkshortener/api/model/Link.java index fabaf9b..4383a39 100644 --- a/src/main/java/org/decepticons/linkshortener/api/model/Link.java +++ b/src/main/java/org/decepticons/linkshortener/api/model/Link.java @@ -110,8 +110,12 @@ public void incrementClicks() { @Override public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Link that)) return false; + if (this == o) { + return true; + } + if (!(o instanceof Link that)) { + return false; + } return Objects.equals(id, that.id); } diff --git a/src/main/java/org/decepticons/linkshortener/api/model/User.java b/src/main/java/org/decepticons/linkshortener/api/model/User.java index 561c51e..b9446cc 100644 --- a/src/main/java/org/decepticons/linkshortener/api/model/User.java +++ b/src/main/java/org/decepticons/linkshortener/api/model/User.java @@ -41,8 +41,8 @@ *
  • {@code updatedAt} – Timestamp of last update.
  • * * - * @since 1.0 - * author Ruslan Lomaka + * @since 1.0 + * author Ruslan Lomaka */ @Getter diff --git a/src/main/java/org/decepticons/linkshortener/api/repository/LinkRepository.java b/src/main/java/org/decepticons/linkshortener/api/repository/LinkRepository.java index 534a75c..aed11f6 100644 --- a/src/main/java/org/decepticons/linkshortener/api/repository/LinkRepository.java +++ b/src/main/java/org/decepticons/linkshortener/api/repository/LinkRepository.java @@ -56,11 +56,19 @@ public interface LinkRepository extends JpaRepository { Page findAllByOwnerIdAndStatus(UUID ownerId, LinkStatus status, Pageable pageable); + /** + * Increments the click count and updates the last accessed timestamp for a link + * identified by its short code using a native SQL query. + * + * @param code the short code of the link to update + * @return the number of rows affected (should be 1 if the link exists, 0 otherwise) + */ @Modifying(clearAutomatically = true, flushAutomatically = true) @Query(value = """ - UPDATE links - SET clicks = clicks + 1, last_accessed_at = now() - WHERE code = :code -""", nativeQuery = true) + UPDATE links + SET clicks = clicks + 1, last_accessed_at = now() + WHERE code = :code + """, nativeQuery = true) int incrementClicksByCodeNative(@Param("code") String code); + } \ No newline at end of file diff --git a/src/main/java/org/decepticons/linkshortener/api/v1/controller/unversioned/GlobalExceptionHandlerController.java b/src/main/java/org/decepticons/linkshortener/api/v1/controller/unversioned/GlobalExceptionHandlerController.java index 949062f..0c61b9b 100644 --- a/src/main/java/org/decepticons/linkshortener/api/v1/controller/unversioned/GlobalExceptionHandlerController.java +++ b/src/main/java/org/decepticons/linkshortener/api/v1/controller/unversioned/GlobalExceptionHandlerController.java @@ -1,4 +1,4 @@ -package org.decepticons.linkshortener.api.controller; +package org.decepticons.linkshortener.api.v1.controller.unversioned; import java.time.Instant; import java.util.Map; diff --git a/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java b/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java index af33ade..475783a 100644 --- a/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java @@ -1,6 +1,18 @@ package org.decepticons.linkshortener.api.controller; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.UUID; import org.decepticons.linkshortener.api.dto.LinkResponseDto; import org.decepticons.linkshortener.api.dto.UpdateLinkExpirationRequestDto; import org.decepticons.linkshortener.api.dto.UrlRequestDto; @@ -24,13 +36,6 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.List; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -59,12 +64,11 @@ void setUp() { @Test @DisplayName("Create Short Link - Success") - void testCreateShortLink_Success() { + void testCreateShortLinkSuccess() { UrlRequestDto urlRequestDto = new UrlRequestDto(); urlRequestDto.setUrl("https://example.com/some/long/url"); User fakeUser = new User(); -// fakeUser.setId(UUID.randomUUID()); fakeUser.setUsername("someName"); @@ -99,7 +103,7 @@ void testCreateShortLink_Success() { @Test @DisplayName("Create Short Link - User Not Found") - void testCreateShortLink_UserNotFound_Controller() { + void testCreateShortLinkUserNotFoundController() { UrlRequestDto urlRequestDto = new UrlRequestDto(); urlRequestDto.setUrl("https://example.com/some/long/url"); @@ -121,7 +125,7 @@ void testCreateShortLink_UserNotFound_Controller() { @Test @DisplayName("Get All My Links - Success") - void getAllMyLinks_SUCCESS() { + void getAllMyLinksSuccess() { int page = 0; int size = 10; @@ -165,7 +169,7 @@ void getAllMyLinks_SUCCESS() { @Test @DisplayName("Get All My Active Links - Success") - void getAllMyActiveLinks_SUCCESS() { + void getAllMyActiveLinksSuccess() { int page = 0; int size = 10; @@ -214,7 +218,7 @@ void getAllMyActiveLinks_SUCCESS() { @Test @DisplayName("Delete Link - Success") - void testDeleteLink_SUCCESS() { + void testDeleteLinkSuccess() { UUID linkId = UUID.randomUUID(); String mockCode = "abc123"; @@ -233,7 +237,7 @@ void testDeleteLink_SUCCESS() { @Test @DisplayName("Update Link Expiration Date - Success") - void testUpdateLinkExpirationDate(){ + void testUpdateLinkExpirationDate() { String code = "abc123"; @@ -253,7 +257,8 @@ void testUpdateLinkExpirationDate(){ UUID.randomUUID() )); - ResponseEntity response = linkController.updateLinkExpiration(requestDto, code); + ResponseEntity response = + linkController.updateLinkExpiration(requestDto, code); assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody()); diff --git a/src/test/java/org/decepticons/linkshortener/api/exceptions/GlobalExceptionHandlerTest.java b/src/test/java/org/decepticons/linkshortener/api/exceptions/GlobalExceptionHandlerTest.java index 925b84c..50ded9a 100644 --- a/src/test/java/org/decepticons/linkshortener/api/exceptions/GlobalExceptionHandlerTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/exceptions/GlobalExceptionHandlerTest.java @@ -1,8 +1,17 @@ package org.decepticons.linkshortener.api.exceptions; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import io.jsonwebtoken.JwtException; -import org.decepticons.linkshortener.api.controller.GlobalExceptionHandlerController; -import org.decepticons.linkshortener.api.exception.*; +import org.decepticons.linkshortener.api.exception.ExpiredTokenException; +import org.decepticons.linkshortener.api.exception.InvalidPasswordException; +import org.decepticons.linkshortener.api.exception.InvalidTokenException; +import org.decepticons.linkshortener.api.exception.UserAlreadyExistsException; +import org.decepticons.linkshortener.api.exception.UserNotFoundException; +import org.decepticons.linkshortener.api.v1.controller.unversioned.GlobalExceptionHandlerController; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -13,10 +22,6 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - /** * Unit tests for the GlobalExceptionHandler. * These tests ensure that all custom exceptions are handled correctly @@ -25,7 +30,7 @@ @DisplayName("Global Exception Handler Tests") class GlobalExceptionHandlerTest { - private MockMvc mockMvc; + private MockMvc mockMvc; @BeforeEach void setup() { diff --git a/src/test/java/org/decepticons/linkshortener/api/model/RoleTest.java b/src/test/java/org/decepticons/linkshortener/api/model/RoleTest.java index 050b6d1..a1511fa 100644 --- a/src/test/java/org/decepticons/linkshortener/api/model/RoleTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/model/RoleTest.java @@ -1,9 +1,12 @@ package org.decepticons.linkshortener.api.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; @DisplayName("Role Class Unit Tests") class RoleTest { diff --git a/src/test/java/org/decepticons/linkshortener/api/security/controller/AuthControllerTest.java b/src/test/java/org/decepticons/linkshortener/api/security/controller/AuthControllerTest.java index c830d4b..21f1bef 100644 --- a/src/test/java/org/decepticons/linkshortener/api/security/controller/AuthControllerTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/security/controller/AuthControllerTest.java @@ -1,5 +1,10 @@ package org.decepticons.linkshortener.api.security.controller; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import com.fasterxml.jackson.databind.ObjectMapper; import org.decepticons.linkshortener.api.dto.AuthRequestDto; import org.decepticons.linkshortener.api.dto.RegistrationRequestDto; @@ -22,14 +27,6 @@ import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; -import java.util.UUID; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - /** * Integration tests for the AuthController using Testcontainers. * These tests verify the full application stack's behavior for authentication @@ -40,174 +37,176 @@ @DisplayName("Auth Controller Integration Tests with Testcontainers") class AuthControllerTest { - private MockMvc mockMvc; + private MockMvc mockMvc; - @Autowired - private WebApplicationContext webApplicationContext; + @Autowired + private WebApplicationContext webApplicationContext; - @Autowired - private UserRepository userRepository; + @Autowired + private UserRepository userRepository; - @Autowired - private PasswordEncoder passwordEncoder; + @Autowired + private PasswordEncoder passwordEncoder; - private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectMapper objectMapper = new ObjectMapper(); - // Use a PostgreSQL container for the tests - @Container - public static PostgreSQLContainer postgresContainer = new PostgreSQLContainer<>("postgres:16-alpine") - .withDatabaseName("testdb") - .withUsername("testuser") - .withPassword("testpass"); + // Use a PostgreSQL container for the tests + @Container + public static PostgreSQLContainer postgresContainer = new PostgreSQLContainer<>( + "postgres:16-alpine") + .withDatabaseName("testdb") + .withUsername("testuser") + .withPassword("testpass"); - // Dynamically set the data source properties using the running container's details - @DynamicPropertySource + // Dynamically set the data source properties using the running container's details + @DynamicPropertySource static void setDatasourceProperties(DynamicPropertyRegistry registry) { - registry.add("spring.datasource.url", postgresContainer::getJdbcUrl); - registry.add("spring.datasource.username", postgresContainer::getUsername); - registry.add("spring.datasource.password", postgresContainer::getPassword); - registry.add("spring.datasource.driver-class-name", () -> "org.postgresql.Driver"); - registry.add("spring.flyway.enabled", () -> "true"); - registry.add("spring.flyway.locations", () -> "classpath:db/migration/postgresql"); - } - - @BeforeEach - void setup() { - mockMvc = MockMvcBuilders - .webAppContextSetup(webApplicationContext) - .build(); - userRepository.deleteAll(); // Clean up before each test - } - - @Test - @DisplayName("given a new user, when registering, then the user is successfully created and returns 200 OK") - void givenNewUser_whenRegistering_thenUserIsCreated() throws Exception { - // Given a new registration request - RegistrationRequestDto requestDto = new RegistrationRequestDto(); - requestDto.setUsername("testuser_new"); - requestDto.setPassword("Password123!"); - - // When the registration endpoint is called - mockMvc.perform(post("/api/v1/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(requestDto))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$").value("testuser_new")); - - // Then verify the user exists in the database - assertTrue(userRepository.findByUsername("testuser_new").isPresent()); - } - - @Test - @DisplayName("given an existing user, when registering, then returns 409 Conflict") - void givenExistingUser_whenRegistering_thenReturnsConflict() throws Exception { - // Given an existing user in the database - User existingUser = new User(); - existingUser.setUsername("existinguser"); - existingUser.setPasswordHash(passwordEncoder.encode("Password123!")); - userRepository.save(existingUser); - - // When attempting to register the same user again - RegistrationRequestDto requestDto = new RegistrationRequestDto(); - requestDto.setUsername("existinguser"); - requestDto.setPassword("Password123!"); - - // Then an exception should be thrown - mockMvc.perform(post("/api/v1/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(requestDto))) - .andExpect(status().isConflict()) - .andExpect(jsonPath("$.message").value("User with username 'existinguser' already exists")); - } - - @Test - @DisplayName("given valid credentials, when logging in, then returns JWT tokens") - void givenValidCredentials_whenLoggingIn_thenReturnsJwtTokens() throws Exception { - // Given a registered user - RegistrationRequestDto registrationRequestDto = new RegistrationRequestDto(); - registrationRequestDto.setUsername("loginuser"); - registrationRequestDto.setPassword("Password123!"); - mockMvc.perform(post("/api/v1/auth/register") + registry.add("spring.datasource.url", postgresContainer::getJdbcUrl); + registry.add("spring.datasource.username", postgresContainer::getUsername); + registry.add("spring.datasource.password", postgresContainer::getPassword); + registry.add("spring.datasource.driver-class-name", () -> "org.postgresql.Driver"); + registry.add("spring.flyway.enabled", () -> "true"); + registry.add("spring.flyway.locations", () -> "classpath:db/migration/postgresql"); + } + + @BeforeEach + void setup() { + mockMvc = MockMvcBuilders + .webAppContextSetup(webApplicationContext) + .build(); + userRepository.deleteAll(); // Clean up before each test + } + + @Test + @DisplayName("given a new user, when registering, then the user " + + "is successfully created and returns 200 OK") + void givenNewUser_whenRegistering_thenUserIsCreated() throws Exception { + // Given a new registration request + RegistrationRequestDto requestDto = new RegistrationRequestDto(); + requestDto.setUsername("testuser_new"); + requestDto.setPassword("Password123!"); + + // When the registration endpoint is called + mockMvc.perform(post("/api/v1/auth/register") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(registrationRequestDto))); - - // When logging in with valid credentials - AuthRequestDto loginRequestDto = new AuthRequestDto(); - loginRequestDto.setUsername("loginuser"); - loginRequestDto.setPassword("Password123!"); - - // Then the response should contain access and refresh tokens - mockMvc.perform(post("/api/v1/auth/login") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(loginRequestDto))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.username").value("loginuser")) - .andExpect(jsonPath("$.accessToken").exists()) - .andExpect(jsonPath("$.refreshToken").exists()); - } - - @Test - @DisplayName("given invalid password, when logging in, then returns 401 Unauthorized") - void givenInvalidPassword_whenLoggingIn_thenReturnsUnauthorized() throws Exception { - // Given a registered user - RegistrationRequestDto registrationRequestDto = new RegistrationRequestDto(); - registrationRequestDto.setUsername("badpassuser"); - registrationRequestDto.setPassword("Password123!"); - mockMvc.perform(post("/api/v1/auth/register") + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").value("testuser_new")); + + // Then verify the user exists in the database + assertTrue(userRepository.findByUsername("testuser_new").isPresent()); + } + + @Test + @DisplayName("given an existing user, when registering, then returns 409 Conflict") + void givenExistingUser_whenRegistering_thenReturnsConflict() throws Exception { + // Given an existing user in the database + User existingUser = new User(); + existingUser.setUsername("existinguser"); + existingUser.setPasswordHash(passwordEncoder.encode("Password123!")); + userRepository.save(existingUser); + + // When attempting to register the same user again + RegistrationRequestDto requestDto = new RegistrationRequestDto(); + requestDto.setUsername("existinguser"); + requestDto.setPassword("Password123!"); + + // Then an exception should be thrown + mockMvc.perform(post("/api/v1/auth/register") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(registrationRequestDto))); - - // When logging in with an invalid password - AuthRequestDto loginRequestDto = new AuthRequestDto(); - loginRequestDto.setUsername("badpassuser"); - loginRequestDto.setPassword("WrongPassword"); - - // Then the response should be 401 Unauthorized - mockMvc.perform(post("/api/v1/auth/login") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(loginRequestDto))) - .andExpect(status().isUnauthorized()); - } - - @Test - @DisplayName("given a valid refresh token, when refreshing, then returns new JWT tokens") - void givenValidRefreshToken_whenRefreshing_thenReturnsNewTokens() throws Exception { - // Given a registered user with a valid refresh token - RegistrationRequestDto registrationRequestDto = new RegistrationRequestDto(); - registrationRequestDto.setUsername("refreshuser"); - registrationRequestDto.setPassword("Password123!"); - mockMvc.perform(post("/api/v1/auth/register") + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.message").value("User with username 'existinguser' already exists")); + } + + @Test + @DisplayName("given valid credentials, when logging in, then returns JWT tokens") + void givenValidCredentials_whenLoggingIn_thenReturnsJwtTokens() throws Exception { + // Given a registered user + RegistrationRequestDto registrationRequestDto = new RegistrationRequestDto(); + registrationRequestDto.setUsername("loginuser"); + registrationRequestDto.setPassword("Password123!"); + mockMvc.perform(post("/api/v1/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(registrationRequestDto))); + + // When logging in with valid credentials + AuthRequestDto loginRequestDto = new AuthRequestDto(); + loginRequestDto.setUsername("loginuser"); + loginRequestDto.setPassword("Password123!"); + + // Then the response should contain access and refresh tokens + mockMvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginRequestDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.username").value("loginuser")) + .andExpect(jsonPath("$.accessToken").exists()) + .andExpect(jsonPath("$.refreshToken").exists()); + } + + @Test + @DisplayName("given invalid password, when logging in, then returns 401 Unauthorized") + void givenInvalidPassword_whenLoggingIn_thenReturnsUnauthorized() throws Exception { + // Given a registered user + RegistrationRequestDto registrationRequestDto = new RegistrationRequestDto(); + registrationRequestDto.setUsername("badpassuser"); + registrationRequestDto.setPassword("Password123!"); + mockMvc.perform(post("/api/v1/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(registrationRequestDto))); + + // When logging in with an invalid password + AuthRequestDto loginRequestDto = new AuthRequestDto(); + loginRequestDto.setUsername("badpassuser"); + loginRequestDto.setPassword("WrongPassword"); + + // Then the response should be 401 Unauthorized + mockMvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginRequestDto))) + .andExpect(status().isUnauthorized()); + } + + @Test + @DisplayName("given a valid refresh token, when refreshing, then returns new JWT tokens") + void givenValidRefreshToken_whenRefreshing_thenReturnsNewTokens() throws Exception { + // Given a registered user with a valid refresh token + RegistrationRequestDto registrationRequestDto = new RegistrationRequestDto(); + registrationRequestDto.setUsername("refreshuser"); + registrationRequestDto.setPassword("Password123!"); + mockMvc.perform(post("/api/v1/auth/register") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(registrationRequestDto))); - AuthRequestDto loginRequestDto = new AuthRequestDto(); - loginRequestDto.setUsername("refreshuser"); - loginRequestDto.setPassword("Password123!"); - MvcResult result = mockMvc.perform(post("/api/v1/auth/login") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(loginRequestDto))) - .andReturn(); - - String response = result.getResponse().getContentAsString(); - String refreshToken = objectMapper.readTree(response).get("refreshToken").asText(); - - // When refreshing the token - mockMvc.perform(post("/api/v1/auth/refresh") - .header("Authorization", "Bearer " + refreshToken)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.accessToken").exists()) - .andExpect(jsonPath("$.refreshToken").exists()); - } - - @Test - @DisplayName("given an invalid refresh token, when refreshing, then returns 401 Unauthorized") - void givenInvalidRefreshToken_whenRefreshing_thenReturnsUnauthorized() throws Exception { - // Given an invalid token - String invalidToken = "invalid.token.123"; - - // When refreshing with the invalid token - mockMvc.perform(post("/api/v1/auth/refresh") - .header("Authorization", "Bearer " + invalidToken)) - .andExpect(status().isUnauthorized()); - } + AuthRequestDto loginRequestDto = new AuthRequestDto(); + loginRequestDto.setUsername("refreshuser"); + loginRequestDto.setPassword("Password123!"); + MvcResult result = mockMvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginRequestDto))) + .andReturn(); + + String response = result.getResponse().getContentAsString(); + String refreshToken = objectMapper.readTree(response).get("refreshToken").asText(); + + // When refreshing the token + mockMvc.perform(post("/api/v1/auth/refresh") + .header("Authorization", "Bearer " + refreshToken)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.accessToken").exists()) + .andExpect(jsonPath("$.refreshToken").exists()); + } + + @Test + @DisplayName("given an invalid refresh token, when refreshing, then returns 401 Unauthorized") + void givenInvalidRefreshToken_whenRefreshing_thenReturnsUnauthorized() throws Exception { + // Given an invalid token + String invalidToken = "invalid.token.123"; + + // When refreshing with the invalid token + mockMvc.perform(post("/api/v1/auth/refresh") + .header("Authorization", "Bearer " + invalidToken)) + .andExpect(status().isUnauthorized()); + } } \ No newline at end of file diff --git a/src/test/java/org/decepticons/linkshortener/api/security/jwt/JwtAuthenticationFilterTest.java b/src/test/java/org/decepticons/linkshortener/api/security/jwt/JwtAuthenticationFilterTest.java index 06787a3..e1c8d2d 100644 --- a/src/test/java/org/decepticons/linkshortener/api/security/jwt/JwtAuthenticationFilterTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/security/jwt/JwtAuthenticationFilterTest.java @@ -1,9 +1,20 @@ package org.decepticons.linkshortener.api.security.jwt; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; import org.decepticons.linkshortener.api.exception.InvalidTokenException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -17,13 +28,6 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; -import java.io.IOException; -import java.util.ArrayList; - -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.*; /** * Unit tests for the JwtAuthenticationFilter. @@ -52,7 +56,8 @@ class JwtAuthenticationFilterTest { @InjectMocks private JwtAuthenticationFilter jwtAuthenticationFilter; - private final String authHeader = "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0dXNlciJ9.invalid-signature"; + private final String authHeader + = "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0dXNlciJ9.invalid-signature"; private UserDetails userDetails; @BeforeEach @@ -64,7 +69,8 @@ void setUp() { @Test @DisplayName("given a valid token, when filtering, then authenticates the user successfully") - void givenValidToken_whenFiltering_thenAuthenticatesSuccessfully() throws ServletException, IOException { + void givenValidToken_whenFiltering_thenAuthenticatesSuccessfully() + throws ServletException, IOException { // Given when(request.getHeader("Authorization")).thenReturn(authHeader); when(jwtTokenUtil.extractUsername(anyString())).thenReturn("testuser"); @@ -96,7 +102,8 @@ void givenNoHeader_whenFiltering_thenSkipsAuthentication() throws ServletExcepti @Test @DisplayName("given a malformed header, when filtering, then throws InvalidTokenException") - void givenMalformedHeader_whenFiltering_thenThrowsInvalidTokenException() throws ServletException, IOException { + void givenMalformedHeader_whenFiltering_thenThrowsInvalidTokenException() + throws ServletException, IOException { // Given when(request.getHeader("Authorization")).thenReturn("MalformedToken"); @@ -110,7 +117,8 @@ void givenMalformedHeader_whenFiltering_thenThrowsInvalidTokenException() throws @Test @DisplayName("given an invalid token, when filtering, then throws InvalidTokenException") - void givenInvalidToken_whenFiltering_thenThrowsInvalidTokenException() throws ServletException, IOException { + void givenInvalidToken_whenFiltering_thenThrowsInvalidTokenException() + throws ServletException, IOException { // Given when(request.getHeader("Authorization")).thenReturn(authHeader); when(jwtTokenUtil.extractUsername(anyString())).thenReturn("testuser"); @@ -125,8 +133,10 @@ void givenInvalidToken_whenFiltering_thenThrowsInvalidTokenException() throws Se } @Test - @DisplayName("given a valid token but a nonexistent user, when filtering, then skips authentication") - void givenTokenForNonexistentUser_whenFiltering_thenSkipsAuthentication() throws ServletException, IOException { + @DisplayName("given a valid token but a nonexistent user, " + + "when filtering, then skips authentication") + void givenTokenForNonexistentUser_whenFiltering_thenSkipsAuthentication() + throws ServletException, IOException { // Given when(request.getHeader("Authorization")).thenReturn(authHeader); when(jwtTokenUtil.extractUsername(anyString())).thenReturn("nonexistentuser"); diff --git a/src/test/java/org/decepticons/linkshortener/api/security/jwt/JwtTokenUtilTest.java b/src/test/java/org/decepticons/linkshortener/api/security/jwt/JwtTokenUtilTest.java index 77e15d8..642d18b 100644 --- a/src/test/java/org/decepticons/linkshortener/api/security/jwt/JwtTokenUtilTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/security/jwt/JwtTokenUtilTest.java @@ -1,19 +1,23 @@ package org.decepticons.linkshortener.api.security.jwt; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; +import java.util.Date; +import javax.crypto.SecretKey; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.security.core.userdetails.UserDetails; -import java.util.Date; -import javax.crypto.SecretKey; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; /** * Unit tests for the JwtTokenUtil class. diff --git a/src/test/java/org/decepticons/linkshortener/api/security/model/CustomUserDetailsTest.java b/src/test/java/org/decepticons/linkshortener/api/security/model/CustomUserDetailsTest.java index 2c93b10..6ebb2f1 100644 --- a/src/test/java/org/decepticons/linkshortener/api/security/model/CustomUserDetailsTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/security/model/CustomUserDetailsTest.java @@ -1,12 +1,16 @@ package org.decepticons.linkshortener.api.security.model; -import org.decepticons.linkshortener.api.model.User; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collections; import org.decepticons.linkshortener.api.model.Role; +import org.decepticons.linkshortener.api.model.User; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import java.util.Collections; -import static org.junit.jupiter.api.Assertions.*; @DisplayName("CustomUserDetails Unit Tests") class CustomUserDetailsTest { diff --git a/src/test/java/org/decepticons/linkshortener/api/security/service/impl/AuthServiceImplTest.java b/src/test/java/org/decepticons/linkshortener/api/security/service/impl/AuthServiceImplTest.java index d7204fa..41bf74d 100644 --- a/src/test/java/org/decepticons/linkshortener/api/security/service/impl/AuthServiceImplTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/security/service/impl/AuthServiceImplTest.java @@ -1,10 +1,22 @@ package org.decepticons.linkshortener.api.security.service.impl; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.util.Collections; +import java.util.Optional; import org.decepticons.linkshortener.api.exception.InvalidTokenException; import org.decepticons.linkshortener.api.exception.UserAlreadyExistsException; -import org.decepticons.linkshortener.api.model.User; import org.decepticons.linkshortener.api.model.Role; +import org.decepticons.linkshortener.api.model.User; import org.decepticons.linkshortener.api.model.UserStatus; import org.decepticons.linkshortener.api.repository.RoleRepository; import org.decepticons.linkshortener.api.repository.UserRepository; @@ -21,15 +33,10 @@ import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.password.PasswordEncoder; -import java.util.Collections; -import java.util.Optional; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; /** * Unit tests for the AuthServiceImpl class. @@ -111,7 +118,8 @@ void givenInvalidCredentials_whenLogin_thenThrowsBadCredentialsException() { .thenThrow(new BadCredentialsException("Invalid credentials")); // When & Then - assertThrows(BadCredentialsException.class, () -> authService.login("testuser", "wrongpassword")); + assertThrows(BadCredentialsException.class, + () -> authService.login("testuser", "wrongpassword")); verify(authenticationManager).authenticate(any(UsernamePasswordAuthenticationToken.class)); verify(userAuthService, never()).findByUsername(anyString()); } @@ -135,7 +143,8 @@ void givenValidRefreshToken_whenRefreshing_thenReturnsUser() { } @Test - @DisplayName("given a null or malformed header, when refreshing, then throws InvalidTokenException") + @DisplayName("given a null or malformed header, " + + "when refreshing, then throws InvalidTokenException") void givenMalformedHeader_whenRefreshing_thenThrowsInvalidTokenException() { // When & Then assertThrows(InvalidTokenException.class, () -> authService.refreshToken(null)); diff --git a/src/test/java/org/decepticons/linkshortener/api/security/service/impl/UserAuthServiceImplTest.java b/src/test/java/org/decepticons/linkshortener/api/security/service/impl/UserAuthServiceImplTest.java index 0340dd3..f142b4d 100644 --- a/src/test/java/org/decepticons/linkshortener/api/security/service/impl/UserAuthServiceImplTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/security/service/impl/UserAuthServiceImplTest.java @@ -1,6 +1,15 @@ package org.decepticons.linkshortener.api.security.service.impl; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.util.Optional; import org.decepticons.linkshortener.api.exception.InvalidPasswordException; import org.decepticons.linkshortener.api.exception.UserAlreadyExistsException; import org.decepticons.linkshortener.api.exception.UserNotFoundException; @@ -15,13 +24,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.crypto.password.PasswordEncoder; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; - /** * Unit tests for the UserAuthServiceImpl class. * These tests focus on the user registration and retrieval logic using mocks for dependencies. @@ -95,7 +97,8 @@ void givenInvalidPassword_whenRegistering_thenThrowsInvalidPasswordException() { when(userRepository.existsByUsername(anyString())).thenReturn(false); // When & Then - assertThrows(InvalidPasswordException.class, () -> userAuthService.registerUser(invalidPasswordUser)); + assertThrows(InvalidPasswordException.class, + () -> userAuthService.registerUser(invalidPasswordUser)); verify(userRepository, never()).save(any(User.class)); } @@ -115,13 +118,15 @@ void givenExistingUsername_whenFindingByUsername_thenReturnsUser() { } @Test - @DisplayName("given a nonexistent username, when finding by username, then throws UserNotFoundException") + @DisplayName("given a nonexistent username, " + + "when finding by username, then throws UserNotFoundException") void givenNonexistentUsername_whenFindingByUsername_thenThrowsUserNotFoundException() { // Given when(userRepository.findByUsername("nonexistentuser")).thenReturn(Optional.empty()); // When & Then - assertThrows(UserNotFoundException.class, () -> userAuthService.findByUsername("nonexistentuser")); + assertThrows(UserNotFoundException.class, + () -> userAuthService.findByUsername("nonexistentuser")); verify(userRepository).findByUsername("nonexistentuser"); } } \ No newline at end of file diff --git a/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplMethodsTest.java b/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplMethodsTest.java index 958cf84..72edfb9 100644 --- a/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplMethodsTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplMethodsTest.java @@ -1,5 +1,16 @@ package org.decepticons.linkshortener.api.service; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; import org.decepticons.linkshortener.api.dto.LinkResponseDto; import org.decepticons.linkshortener.api.model.Link; import org.decepticons.linkshortener.api.model.LinkStatus; @@ -11,25 +22,22 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.junit.jupiter.api.extension.ExtendWith; - import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; - -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import org.springframework.test.util.ReflectionTestUtils; +/** * Unit tests for the LinkServiceImpl class. + * These tests focus on the link management logic using mocks for dependencies. + */ @ExtendWith(MockitoExtension.class) public class LinkServiceImplMethodsTest { @Mock @@ -54,7 +62,6 @@ void setUpSecurityContext() { User testUser = new User(); -// testUser.setId(testUserId); testUser.setUsername(testUsername); } @@ -65,7 +72,7 @@ void clearSecurityContext() { @Test @DisplayName("Get All My Links - Success") - void getAllMyLinks_SUCCESS() { + void getAllMyLinksSuccess() { Link link1 = new Link(); link1.setOwner(new User()); @@ -85,7 +92,7 @@ void getAllMyLinks_SUCCESS() { @Test @DisplayName("Get All My Active Links - Success") - void getAllMyActiveLinks_SUCCESS() { + void getAllMyActiveLinksSuccess() { Link link1 = new Link(); link1.setStatus(LinkStatus.ACTIVE); link1.setOwner(new User()); @@ -93,7 +100,8 @@ void getAllMyActiveLinks_SUCCESS() { Page mockPage = new PageImpl<>(List.of(link1)); when(userServiceImpl.getCurrentUserId()).thenReturn(testUserId); - when(linkRepository.findAllByOwnerIdAndStatus(eq(testUserId), eq(LinkStatus.ACTIVE), any(Pageable.class))) + when(linkRepository.findAllByOwnerIdAndStatus(eq(testUserId), + eq(LinkStatus.ACTIVE), any(Pageable.class))) .thenReturn(mockPage); Page result = linkService.getAllMyActiveLinks(0, 10); @@ -105,14 +113,14 @@ void getAllMyActiveLinks_SUCCESS() { @Test @DisplayName("Delete Link - Success") - void deleteLink_SUCCESS() { - UUID linkId = UUID.randomUUID(); + void deleteLinkSuccess() { User owner = new User(); -// owner.setId(testUserId); + ReflectionTestUtils.setField(owner, "id", testUserId); owner.setUsername(testUsername); Link link = new Link(); link.setCode("abc123"); link.setOwner(owner); + UUID linkId = UUID.randomUUID(); when(userServiceImpl.getCurrentUserId()).thenReturn(testUserId); when(linkRepository.findById(linkId)).thenReturn(Optional.of(link)); diff --git a/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplTest.java b/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplTest.java index d7d2de2..e6b5956 100644 --- a/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplTest.java @@ -1,5 +1,19 @@ package org.decepticons.linkshortener.api.service; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Optional; +import java.util.UUID; import org.decepticons.linkshortener.api.dto.LinkResponseDto; import org.decepticons.linkshortener.api.dto.UrlRequestDto; import org.decepticons.linkshortener.api.model.Link; @@ -17,18 +31,9 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.util.ReflectionTestUtils; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Optional; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - @ExtendWith(org.mockito.junit.jupiter.MockitoExtension.class) class LinkServiceImplTest { @@ -49,10 +54,9 @@ class LinkServiceImplTest { @Test @DisplayName("Link Creation - Success") - void testLinkCreation_Success() { + void testLinkCreationSuccess() { UrlRequestDto urlRequestDto = new UrlRequestDto(); User user = new User(); -// user.setId(UUID.randomUUID()); urlRequestDto.setUrl("https://www.example.com"); @@ -70,7 +74,12 @@ void testLinkCreation_Success() { @Test @DisplayName("Increment of Clicks - Success") - void incrementOfClicks_Success() { + void incrementOfClicksSuccess() { + Link link = new Link(); + link.setOwner(new User()); + link.setCode("abc123"); + link.setClicks(0); + LinkResponseDto linkResponseDto = new LinkResponseDto( UUID.randomUUID(), "abc123", @@ -81,10 +90,6 @@ void incrementOfClicks_Success() { "ACTIVE", UUID.randomUUID() ); - Link link = new Link(); - link.setOwner(new User()); - link.setCode("abc123"); - link.setClicks(0); when(linkRepository.incrementClicksByCodeNative("abc123")).thenReturn(1); when(linkRepository.findByCode("abc123")).thenReturn(Optional.of(link)); @@ -95,10 +100,9 @@ void incrementOfClicks_Success() { @Test @DisplayName("Mapping to Response DTO - Success") - void testMapToResponse_Success() { + void testMapToResponseSuccess() { UUID userId = UUID.randomUUID(); User owner = new User(); -// owner.setId(userId); Link link = new Link(); link.setCode("abc123"); @@ -122,10 +126,9 @@ void testMapToResponse_Success() { @Test @DisplayName("Get Link By Code - Success") - void getLinkByCode_Success() { + void getLinkByCodeSuccess() { User owner = new User(); -// owner.setId(UUID.randomUUID()); Link link = new Link(); link.setOwner(owner); @@ -151,9 +154,15 @@ void getLinkByCode_Success() { @Test @DisplayName("Deactivate Link - Success") - void deactivateLink_Success() { + void deactivateLinkSuccess() { User owner = new User(); + + Link link = new Link(); + link.setOwner(owner); + link.setCode("abc123"); + link.setStatus(LinkStatus.ACTIVE); + LinkResponseDto linkResponseDto = new LinkResponseDto( UUID.randomUUID(), "abc123", @@ -165,12 +174,6 @@ void deactivateLink_Success() { UUID.randomUUID() ); - Link link = new Link(); - link.setOwner(owner); - link.setCode("abc123"); - link.setStatus(LinkStatus.ACTIVE); - - when(linkRepository.findByCode("abc123")).thenReturn(Optional.of(link)); when(linkRepository.save(any(Link.class))).thenAnswer(i -> i.getArguments()[0]); @@ -182,7 +185,7 @@ void deactivateLink_Success() { @Test @DisplayName("Validate Link - Success") - void validateLink_Success() { + void validateLinkSuccess() { LinkResponseDto linkResponseDto = new LinkResponseDto( UUID.randomUUID(), "abc123", @@ -201,11 +204,11 @@ void validateLink_Success() { @Test @DisplayName("Update Link Expiration - Success") - void testUpdateLinkExpiration(){ + void testUpdateLinkExpiration() { String code = "abc123"; User owner = new User(); owner.setUsername("testUser"); -// owner.setId(UUID.randomUUID()); + ReflectionTestUtils.setField(owner, "id", UUID.randomUUID()); Link link = new Link(); link.setCode(code); diff --git a/src/test/java/org/decepticons/linkshortener/api/service/UserServiceImplTest.java b/src/test/java/org/decepticons/linkshortener/api/service/UserServiceImplTest.java index c0f47be..5003d0f 100644 --- a/src/test/java/org/decepticons/linkshortener/api/service/UserServiceImplTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/service/UserServiceImplTest.java @@ -1,5 +1,14 @@ package org.decepticons.linkshortener.api.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import java.util.UUID; import org.decepticons.linkshortener.api.exception.NoSuchUserFoundInTheSystemException; import org.decepticons.linkshortener.api.model.User; import org.decepticons.linkshortener.api.repository.UserRepository; @@ -12,18 +21,14 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; - -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.when; +import org.springframework.test.util.ReflectionTestUtils; +/** + * Unit tests for the UserServiceImpl class. + * These tests focus on the user retrieval logic using mocks for dependencies. + */ @ExtendWith(MockitoExtension.class) public class UserServiceImplTest { @Mock @@ -54,9 +59,8 @@ void tearDown() { @Test @DisplayName("Test getCurrentUser returns correct User") - void testGetCurrentUser_Success() { + void testGetCurrentUserSuccess() { User fakeUser = new User(); -// fakeUser.setId(UUID.randomUUID()); fakeUser.setUsername("testuser"); when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(fakeUser)); @@ -69,10 +73,10 @@ void testGetCurrentUser_Success() { @Test @DisplayName("Test getCurrentUserId returns correct UUID") - void testGetCurrentUserId_Success() { + void testGetCurrentUserIdSuccess() { UUID fakeId = UUID.randomUUID(); User fakeUser = new User(); -// fakeUser.setId(fakeId); + ReflectionTestUtils.setField(fakeUser, "id", fakeId); fakeUser.setUsername("testuser"); when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(fakeUser)); @@ -84,7 +88,7 @@ void testGetCurrentUserId_Success() { @Test @DisplayName("Test getCurrentUser throws exception when user not found") - void testGetCurrentUser_UserNotFound() { + void testGetCurrentUserUserNotFound() { when(userRepository.findByUsername("testuser")).thenReturn(Optional.empty()); NoSuchUserFoundInTheSystemException ex = assertThrows( diff --git a/src/test/java/org/decepticons/linkshortener/api/util/AuthMapperTest.java b/src/test/java/org/decepticons/linkshortener/api/util/AuthMapperTest.java index 70503ba..178e322 100644 --- a/src/test/java/org/decepticons/linkshortener/api/util/AuthMapperTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/util/AuthMapperTest.java @@ -1,17 +1,19 @@ package org.decepticons.linkshortener.api.util; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.Collections; +import java.util.List; import org.decepticons.linkshortener.api.dto.AuthResponseDto; import org.decepticons.linkshortener.api.dto.RegistrationRequestDto; -import org.decepticons.linkshortener.api.model.User; import org.decepticons.linkshortener.api.model.Role; +import org.decepticons.linkshortener.api.model.User; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.security.core.GrantedAuthority; -import java.util.Collections; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; @DisplayName("AuthMapper Unit Tests") class AuthMapperTest { @@ -47,7 +49,8 @@ void shouldMapUserAndTokensToAuthResponseDto() { String refreshToken = "mocked.refresh.token"; // When - AuthResponseDto responseDto = AuthMapper.toAuthResponseDto(user, authorities, accessToken, refreshToken); + AuthResponseDto responseDto + = AuthMapper.toAuthResponseDto(user, authorities, accessToken, refreshToken); // Then assertNotNull(responseDto); diff --git a/src/test/java/org/decepticons/linkshortener/api/util/PasswordValidatorTest.java b/src/test/java/org/decepticons/linkshortener/api/util/PasswordValidatorTest.java index 61ac15a..e5074e2 100644 --- a/src/test/java/org/decepticons/linkshortener/api/util/PasswordValidatorTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/util/PasswordValidatorTest.java @@ -1,10 +1,12 @@ package org.decepticons.linkshortener.api.util; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + + /** * Unit tests for the PasswordValidator class. * These tests ensure that the password complexity rules are correctly enforced. diff --git a/src/test/java/org/decepticons/linkshortener/api/v1/controller/CacheControllerTest.java b/src/test/java/org/decepticons/linkshortener/api/v1/controller/CacheControllerTest.java deleted file mode 100644 index 17cd19d..0000000 --- a/src/test/java/org/decepticons/linkshortener/api/v1/controller/CacheControllerTest.java +++ /dev/null @@ -1,254 +0,0 @@ -package org.decepticons.linkshortener.api.v1.controller; - -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.List; -import java.util.UUID; -import org.decepticons.linkshortener.api.dto.LinkResponseDto; -import org.decepticons.linkshortener.api.dto.UrlRequestDto; -import org.decepticons.linkshortener.api.exception.NoSuchUserFoundInTheSystemException; -import org.decepticons.linkshortener.api.model.User; -import org.decepticons.linkshortener.api.service.CacheInspectionService; -import org.decepticons.linkshortener.api.service.LinkService; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.context.SecurityContextHolder; - - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(org.mockito.junit.jupiter.MockitoExtension.class) - class CacheControllerTest { - @Mock - private CacheInspectionService cacheInspectionService; - - @InjectMocks - private CacheController cacheController; - - @Test - @DisplayName("Inspect Cache - Service Called") - void inspectCache_SUCCESS() { - - cacheController.inspectCache(); - - verify(cacheInspectionService, times(1)).printCache("shortLinksCache"); - } - - @ExtendWith(MockitoExtension.class) - @DisplayName("Short Link Creation Tests") - static - class LinkCrudControllerTest { - - @Mock - private LinkService linkService; - - - @InjectMocks - private LinkCrudController linkController; - - - @BeforeEach - void setUp() { - - SecurityContextHolder.getContext().setAuthentication( - new UsernamePasswordAuthenticationToken("someName", null, null) - ); - - } - - @Test - @DisplayName("Create Short Link - Success") - void testCreateShortLink_Success() { - UrlRequestDto urlRequestDto = new UrlRequestDto(); - urlRequestDto.setUrl("https://example.com/some/long/url"); - - User fakeUser = new User(); - fakeUser.setId(UUID.randomUUID()); - fakeUser.setUsername("someName"); - - - LinkResponseDto fakeResponse = new LinkResponseDto( - UUID.randomUUID(), - "abc123", - urlRequestDto.getUrl(), - Instant.now(), - Instant.now().plus(2, ChronoUnit.DAYS), - 0, - "ACTIVE", - fakeUser.getId() - ); - - when(linkService.getCurrentUser()).thenReturn(fakeUser); - - - when(linkService.createLink(urlRequestDto, fakeUser)).thenReturn(fakeResponse); - - - ResponseEntity response = linkController.createLink(urlRequestDto); - - - assertEquals(HttpStatus.CREATED, response.getStatusCode()); - assertEquals("abc123", response.getBody().code()); - assertEquals("https://example.com/some/long/url", response.getBody().originalUrl()); - - - verify(linkService).getCurrentUser(); - verify(linkService).createLink(urlRequestDto, fakeUser); - - } - - - @Test - @DisplayName("Create Short Link - User Not Found") - void testCreateShortLink_UserNotFound() { - - UrlRequestDto urlRequestDto = new UrlRequestDto(); - urlRequestDto.setUrl("https://example.com/some/long/url"); - - - when(linkService.getCurrentUser()) - .thenThrow(new NoSuchUserFoundInTheSystemException("No such user", "someName")); - - - Exception ex = null; - try { - linkController.createLink(urlRequestDto); - }catch (Exception exception){ - ex = exception; - } - - assertNotNull(ex); - assertInstanceOf(NoSuchUserFoundInTheSystemException.class, ex); - - verify(linkService, Mockito.never()).createLink(Mockito.any(), Mockito.any()); - } - - @Test - @DisplayName("Get All My Links - Success") - void getAllMyLinks_SUCCESS() { - int page = 0; - int size = 10; - - LinkResponseDto link1 = new LinkResponseDto( - UUID.randomUUID(), - "code1", - "https://example.com/1", - Instant.now(), - Instant.now().plusSeconds(3600), - 0, - "ACTIVE", - UUID.randomUUID() - ); - - LinkResponseDto link2 = new LinkResponseDto( - UUID.randomUUID(), - "code2", - "https://example.com/2", - Instant.now(), - Instant.now().plusSeconds(3600), - 0, - "ACTIVE", - UUID.randomUUID() - ); - - Page mockPage = new PageImpl<>(List.of(link1, link2)); - - when(linkService.getAllMyLinks(page, size)).thenReturn(mockPage); - - ResponseEntity> response = linkController.getAllMyLinks(page, size); - - assertNotNull(response.getBody()); - assertEquals(2, response.getBody().getContent().size()); - assertEquals("code1", response.getBody().getContent().get(0).code()); - assertEquals("code2", response.getBody().getContent().get(1).code()); - - - verify(linkService, times(1)).getAllMyLinks(page, size); - - } - - @Test - @DisplayName("Get All My Active Links - Success") - void getAllMyActiveLinks_SUCCESS() { - int page = 0; - int size = 10; - - - LinkResponseDto link1 = new LinkResponseDto( - UUID.randomUUID(), - "code1", - "https://example.com/1", - Instant.now(), - Instant.now().plusSeconds(3600), - 0, - "ACTIVE", - UUID.randomUUID() - ); - - LinkResponseDto link2 = new LinkResponseDto( - UUID.randomUUID(), - "code2", - "https://example.com/2", - Instant.now(), - Instant.now().plusSeconds(3600), - 0, - "ACTIVE", - UUID.randomUUID() - ); - - Page mockPage = new PageImpl<>(List.of(link1, link2)); - - - when(linkService.getAllMyActiveLinks(page, size)).thenReturn(mockPage); - - - ResponseEntity> response = linkController.getAllMyActiveLinks(page, size); - - - assertNotNull(response.getBody()); - assertEquals(2, response.getBody().getContent().size()); - assertEquals("code1", response.getBody().getContent().get(0).code()); - assertEquals("code2", response.getBody().getContent().get(1).code()); - - - verify(linkService, times(1)).getAllMyActiveLinks(page, size); - } - - - - @Test - @DisplayName("Delete Link - Success") - void testDeleteLink_SUCCESS() { - - UUID linkId = UUID.randomUUID(); - - doNothing().when(linkService).deleteLink(linkId); - - ResponseEntity response = linkController.deleteLink(linkId); - - assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode()); - assertNull(response.getBody()); - - verify(linkService, times(1)).deleteLink(linkId); - } - - } -} diff --git a/src/test/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudControllerTest.java b/src/test/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudControllerTest.java index 957385f..0cce143 100644 --- a/src/test/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudControllerTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/v1/controller/LinkCrudControllerTest.java @@ -1,5 +1,18 @@ package org.decepticons.linkshortener.api.v1.controller; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.time.Instant; +import java.util.Collections; +import java.util.UUID; import org.decepticons.linkshortener.api.dto.LinkResponseDto; import org.decepticons.linkshortener.api.service.impl.LinkServiceImpl; import org.junit.jupiter.api.Test; @@ -15,15 +28,6 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; -import java.time.Instant; -import java.util.Collections; -import java.util.UUID; - -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - @SpringBootTest @AutoConfigureMockMvc class LinkCrudControllerTest { diff --git a/src/test/java/org/decepticons/linkshortener/api/v1/controller/LinkShortenerApplicationTests.java b/src/test/java/org/decepticons/linkshortener/api/v1/controller/LinkShortenerApplicationTests.java index 1023eed..bc3020c 100644 --- a/src/test/java/org/decepticons/linkshortener/api/v1/controller/LinkShortenerApplicationTests.java +++ b/src/test/java/org/decepticons/linkshortener/api/v1/controller/LinkShortenerApplicationTests.java @@ -1,5 +1,9 @@ package org.decepticons.linkshortener.api.v1.controller; +import static org.assertj.core.api.Assertions.assertThat; + +import org.decepticons.linkshortener.api.security.jwt.JwtAuthenticationFilter; +import org.decepticons.linkshortener.api.security.jwt.JwtTokenUtil; import org.decepticons.linkshortener.api.v1.controller.unversioned.HealthController; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -7,25 +11,27 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.ApplicationContext; import org.springframework.test.context.bean.override.mockito.MockitoBean; -import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest @AutoConfigureMockMvc(addFilters = false) class LinkShortenerApplicationTests { - @Autowired ApplicationContext context; + @Autowired ApplicationContext context; - @MockitoBean - org.decepticons.linkshortener.api.security.jwt.JwtAuthenticationFilter jwtFilter; - @MockitoBean org.decepticons.linkshortener.api.security.jwt.JwtTokenUtil jwtTokenUtil; + @MockitoBean + JwtAuthenticationFilter jwtFilter; + @MockitoBean + JwtTokenUtil jwtTokenUtil; - @Test - void contextLoads() { assertThat(context).isNotNull(); } + @Test + void contextLoads() { + assertThat(context).isNotNull(); + } - @Test - void healthControllerIsLoaded() { - assertThat(context.getBean( - HealthController.class)).isNotNull(); - } + @Test +void healthControllerIsLoaded() { + assertThat(context.getBean( + HealthController.class)).isNotNull(); + } } diff --git a/src/test/java/org/decepticons/linkshortener/api/v1/controller/unversioned/HealthControllerTest.java b/src/test/java/org/decepticons/linkshortener/api/v1/controller/unversioned/HealthControllerTest.java index bcbc391..17bd01e 100644 --- a/src/test/java/org/decepticons/linkshortener/api/v1/controller/unversioned/HealthControllerTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/v1/controller/unversioned/HealthControllerTest.java @@ -1,5 +1,10 @@ package org.decepticons.linkshortener.api.v1.controller.unversioned; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import org.decepticons.linkshortener.api.security.jwt.JwtAuthenticationFilter; import org.decepticons.linkshortener.api.security.jwt.JwtTokenUtil; import org.junit.jupiter.api.Test; @@ -9,24 +14,22 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @WebMvcTest(controllers = HealthController.class) @AutoConfigureMockMvc(addFilters = false) // disable Spring Security filters class HealthControllerTest { - @Autowired - private MockMvc mvc; + @Autowired + private MockMvc mvc; - @MockitoBean private JwtAuthenticationFilter jwtAuthenticationFilter; - @MockitoBean private JwtTokenUtil jwtTokenUtil; + @MockitoBean private JwtAuthenticationFilter jwtAuthenticationFilter; + @MockitoBean private JwtTokenUtil jwtTokenUtil; - @Test - void health_returnsUp() throws Exception { - mvc.perform(get("/health")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json")) - .andExpect(jsonPath("$.status").value("UP")); - } + @Test + void health_returnsUp() throws Exception { + mvc.perform(get("/health")) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.status").value("UP")); + } } diff --git a/src/test/java/org/decepticons/linkshortener/api/v1/controller/unversioned/RedirectControllerTest.java b/src/test/java/org/decepticons/linkshortener/api/v1/controller/unversioned/RedirectControllerTest.java index f53ed60..0ec74ab 100644 --- a/src/test/java/org/decepticons/linkshortener/api/v1/controller/unversioned/RedirectControllerTest.java +++ b/src/test/java/org/decepticons/linkshortener/api/v1/controller/unversioned/RedirectControllerTest.java @@ -1,8 +1,18 @@ package org.decepticons.linkshortener.api.v1.controller.unversioned; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.time.Instant; import org.decepticons.linkshortener.api.dto.LinkResponseDto; +import org.decepticons.linkshortener.api.exception.ShortLinkIsOutOfDateException; import org.decepticons.linkshortener.api.model.User; import org.decepticons.linkshortener.api.service.impl.LinkServiceImpl; import org.junit.jupiter.api.Test; @@ -11,12 +21,6 @@ import org.mockito.Mock; -import java.io.IOException; -import java.time.Instant; - -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.mockito.Mockito.*; - @ExtendWith(org.mockito.junit.jupiter.MockitoExtension.class) class RedirectControllerTest { @@ -28,10 +32,10 @@ class RedirectControllerTest { private HttpServletResponse httpServletResponse; @Test - void verifyRedirectToOriginalUrl_SUCCESS() throws IOException { + void verifyRedirectToOriginalUrlSuccess() throws IOException { String code = "abc123"; User owner = new User(); -// owner.setId(java.util.UUID.randomUUID()); + LinkResponseDto responseDto = new LinkResponseDto( java.util.UUID.randomUUID(), @@ -58,10 +62,10 @@ void verifyRedirectToOriginalUrl_SUCCESS() throws IOException { } @Test - void verifyExceptionThrownWhenLinkNotValid_SUCCESS() throws IOException { + void verifyExceptionThrownWhenLinkNotValidSuccess() throws IOException { String code = "abc123"; User owner = new User(); -// owner.setId(java.util.UUID.randomUUID()); + LinkResponseDto responseDto = new LinkResponseDto( java.util.UUID.randomUUID(), @@ -75,18 +79,18 @@ void verifyExceptionThrownWhenLinkNotValid_SUCCESS() throws IOException { ); when(linkServiceImpl.getLinkByCode(code)).thenReturn(responseDto); - when(linkServiceImpl.validateLink(responseDto)).thenReturn(false ); + when(linkServiceImpl.validateLink(responseDto)).thenReturn(false); Exception ex = null; try { redirectController.redirect(code, httpServletResponse); - }catch (Exception e) { + } catch (Exception e) { ex = e; } verify(httpServletResponse, never()).sendRedirect("https://www.example.com"); - assertInstanceOf(org.decepticons.linkshortener.api.exception.ShortLinkIsOutOfDateException.class, ex); + assertInstanceOf(ShortLinkIsOutOfDateException.class, ex); } }