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/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/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/exception/InvalidExpirationDateException.java b/src/main/java/org/decepticons/linkshortener/api/exception/InvalidExpirationDateException.java
new file mode 100644
index 0000000..22244c8
--- /dev/null
+++ b/src/main/java/org/decepticons/linkshortener/api/exception/InvalidExpirationDateException.java
@@ -0,0 +1,30 @@
+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;
+
+/**
+ * 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/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/GlobalExceptionHandler.java b/src/main/java/org/decepticons/linkshortener/api/exceptions/GlobalExceptionHandler.java
deleted file mode 100644
index f4940d0..0000000
--- a/src/main/java/org/decepticons/linkshortener/api/exceptions/GlobalExceptionHandler.java
+++ /dev/null
@@ -1,167 +0,0 @@
-package org.decepticons.linkshortener.api.exceptions;
-
-import io.jsonwebtoken.JwtException;
-import java.time.Instant;
-import java.util.Map;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-import org.springframework.security.core.AuthenticationException;
-import org.springframework.web.bind.annotation.ControllerAdvice;
-import org.springframework.web.bind.annotation.ExceptionHandler;
-
-/**
- * Centralized exception handler for REST controllers.
- * This class catches specific exceptions and maps them
- * to appropriate HTTP status codes, ensuring a consistent and
- * informative error response
- * format.
- */
-@ControllerAdvice
-public class GlobalExceptionHandler {
-
- /**
- * Constructs a standard error response body.
- *
- * @param status the HTTP status to return.
- * @param error the short error description.
- * @param message the detailed error message.
- * @return a map representing the error response body.
- */
- private ResponseEntity
+ * 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 UserRepository userRepository;
- private final CacheEvictService cacheEvictService;
- 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,
- UserRepository userRepository,
- CacheEvictService cacheEvictService) {
- this.linkRepository = linkRepository;
- this.userRepository = userRepository;
- this.cacheEvictService = cacheEvictService;
- }
-
+ 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);
- }
-
-
- /**
- * 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
- */
- 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
- */
- public Page getAllMyLinks(int page, int size) {
- UUID userId = getCurrentUserId();
- Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
- return linkRepository.findAllByOwnerId(userId, pageable)
- .map(this::mapToResponse);
- }
+ Page getAllMyActiveLinks(int page, int size);
/**
- * Retrieves all active links of the currently authenticated user with pagination.
+ * Deletes a link by its unique identifier.
*
- * @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
+ * @param linkId the UUID of the link to delete
+ * @return a confirmation message indicating the result of the deletion
*/
- public Page getAllMyActiveLinks(int page, int size) {
- UUID userId = getCurrentUserId();
- Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
- return linkRepository.findAllByOwnerIdAndStatus(userId, LinkStatus.ACTIVE, pageable)
- .map(this::mapToResponse);
- }
+ String deleteLink(UUID linkId);
/**
- * Deletes a link from the database if it belongs to the currently authenticated user.
+ * Updates the expiration date of a link identified by its short code.
*
- * @param linkId the unique identifier of the link to delete
+ * @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
*/
- @Transactional
- public void deleteLink(UUID linkId) {
- UUID currentUserId = getCurrentUserId();
-
- 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
- */
- 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
- ));
- }
+ 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
new file mode 100644
index 0000000..af05c4c
--- /dev/null
+++ b/src/main/java/org/decepticons/linkshortener/api/service/UserService.java
@@ -0,0 +1,25 @@
+package org.decepticons.linkshortener.api.service;
+
+import java.util.UUID;
+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/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/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 f59240a..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
@@ -1,15 +1,18 @@
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;
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.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -18,12 +21,14 @@
import org.springframework.web.bind.annotation.RestController;
+
+
/**
* 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.
*/
+@Tag(name = "Link Management", description = "Operations for managing short links")
@RestController
@RequestMapping("/api/v1/links")
public class LinkCrudController {
@@ -34,12 +39,12 @@ 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;
+
}
/**
@@ -49,10 +54,11 @@ 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();
- LinkResponseDto link = linkService.createLink(originalUrl, user);
+
+ LinkResponseDto link = linkService.createLink(originalUrl);
return ResponseEntity.status(201).body(link);
}
@@ -65,6 +71,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
@@ -80,6 +87,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
@@ -94,8 +102,30 @@ 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();
}
+
+
+
+ /**
+ * 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}")
+ @Operation(summary = "Update the expiration date of a specific link by its code")
+ public ResponseEntity updateLinkExpiration(
+ @Valid @RequestBody UpdateLinkExpirationRequestDto newExpirationDate,
+ @PathVariable String code) {
+
+ return ResponseEntity
+ .ok(linkService.updateLinkExpiration(code, newExpirationDate.getNewExpirationDate()));
+ }
+
+
}
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
new file mode 100644
index 0000000..0c61b9b
--- /dev/null
+++ b/src/main/java/org/decepticons/linkshortener/api/v1/controller/unversioned/GlobalExceptionHandlerController.java
@@ -0,0 +1,228 @@
+package org.decepticons.linkshortener.api.v1.controller.unversioned;
+
+import java.time.Instant;
+import java.util.Map;
+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,
+ 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
+ ));
+ }
+
+ /**
+ * 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,
+ final String message) {
+ return ResponseEntity.status(status).body(Map.of(
+ "timestamp", Instant.now().toString(),
+ "status", status.value(),
+ "error", error,
+ "message", message
+ ));
+ }
+
+
+ /**
+ * Handles exceptions when a user already exists.
+ *
+ * @param ex The UserAlreadyExistsException instance.
+ * @return A ResponseEntity with a CONFLICT status.
+ */
+ @ExceptionHandler(UserAlreadyExistsException.class)
+ public ResponseEntity> handleUserExists(
+ final UserAlreadyExistsException ex) {
+ return buildErrorResponseSecurity(
+ HttpStatus.CONFLICT,
+ "User Already Exists",
+ ex.getMessage());
+ }
+
+ /**
+ * Handles exceptions when a user is not found.
+ *
+ * @param ex The UserNotFoundException instance.
+ * @return A ResponseEntity with a NOT_FOUND status.
+ */
+ @ExceptionHandler(UserNotFoundException.class)
+ public ResponseEntity> handleUserNotFound(
+ final UserNotFoundException ex) {
+ return buildErrorResponseSecurity(
+ HttpStatus.NOT_FOUND,
+ "User Not Found",
+ ex.getMessage());
+ }
+
+ /**
+ * Handles exceptions when an invalid password is provided.
+ *
+ * @param ex The InvalidPasswordException instance.
+ * @return A ResponseEntity with a BAD_REQUEST status.
+ */
+ @ExceptionHandler(InvalidPasswordException.class)
+ public ResponseEntity> handleInvalidPassword(
+ final InvalidPasswordException ex) {
+ return buildErrorResponseSecurity(
+ HttpStatus.BAD_REQUEST,
+ "Invalid Password",
+ ex.getMessage());
+ }
+
+ /**
+ * Handles exceptions for an invalid token.
+ *
+ * @param ex The InvalidTokenException instance.
+ * @return A ResponseEntity with an UNAUTHORIZED status.
+ */
+ @ExceptionHandler(InvalidTokenException.class)
+ public ResponseEntity> handleInvalidToken(
+ final InvalidTokenException ex) {
+ return buildErrorResponseSecurity(
+ HttpStatus.UNAUTHORIZED,
+ "Invalid Token",
+ ex.getMessage());
+ }
+
+ /**
+ * Handles exceptions for an expired token.
+ *
+ * @param ex The ExpiredTokenException instance.
+ * @return A ResponseEntity with an UNAUTHORIZED status.
+ */
+ @ExceptionHandler(ExpiredTokenException.class)
+ public ResponseEntity> handleExpiredToken(
+ final ExpiredTokenException ex) {
+ return buildErrorResponseSecurity(HttpStatus.UNAUTHORIZED,
+ "Expired Token",
+ ex.getMessage());
+ }
+
+ /**
+ * Handles generic exceptions.
+ *
+ * @param ex The generic Exception instance.
+ * @return A ResponseEntity with an INTERNAL_SERVER_ERROR status.
+ */
+ @ExceptionHandler(Exception.class)
+ public ResponseEntity> handleGeneric(final Exception ex) {
+ return buildErrorResponseSecurity(HttpStatus.INTERNAL_SERVER_ERROR,
+ "Server Error",
+ ex.getMessage());
+ }
+
+
+ /**
+ * 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) {
+
+ return buildErrorResponse(
+ HttpStatus.NOT_FOUND,
+ "User Not Found",
+ "No such user in the system",
+ 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) {
+
+ return buildErrorResponse(
+ HttpStatus.NOT_FOUND,
+ "Short Link Not Found",
+ "No such short link in the system",
+ 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) {
+ 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/v1/controller/unversioned/RedirectController.java b/src/main/java/org/decepticons/linkshortener/api/v1/controller/unversioned/RedirectController.java
index 7fd9987..92f8247 100644
--- a/src/main/java/org/decepticons/linkshortener/api/v1/controller/unversioned/RedirectController.java
+++ b/src/main/java/org/decepticons/linkshortener/api/v1/controller/unversioned/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/resources/application.yaml b/src/main/resources/application.yaml
index 8fa7c4b..8947d45 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -8,9 +8,6 @@
# ====================================================
logging:
- level:
- org.springdoc: DEBUG
-
springdoc:
api-docs:
path: /v3/api-docs
@@ -21,7 +18,7 @@ springdoc:
spring:
profiles:
- default: dev
+ default: dev # default profile if none specified
server:
port: ${SERVER_PORT:8080}
@@ -30,8 +27,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
new file mode 100644
index 0000000..475783a
--- /dev/null
+++ b/src/test/java/org/decepticons/linkshortener/api/controller/LinkCrudControllerTest.java
@@ -0,0 +1,269 @@
+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;
+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.impl.UserServiceImpl;
+import org.decepticons.linkshortener.api.v1.controller.LinkCrudController;
+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;
+
+
+
+@ExtendWith(MockitoExtension.class)
+@DisplayName("Short Link Creation Tests")
+class LinkCrudControllerTest {
+
+ @Mock
+ private LinkService linkService;
+
+ @Mock
+ UserServiceImpl userServiceImpl;
+
+
+ @InjectMocks
+ private LinkCrudController linkController;
+
+
+ @BeforeEach
+ void setUp() {
+
+ SecurityContextHolder.getContext().setAuthentication(
+ new UsernamePasswordAuthenticationToken("someName", null, null)
+ );
+
+ }
+
+ @Test
+ @DisplayName("Create Short Link - Success")
+ void testCreateShortLinkSuccess() {
+ UrlRequestDto urlRequestDto = new UrlRequestDto();
+ urlRequestDto.setUrl("https://example.com/some/long/url");
+
+ User fakeUser = new User();
+ 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.createLink(urlRequestDto)).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).createLink(urlRequestDto);
+
+ }
+
+
+ @Test
+ @DisplayName("Create Short Link - User Not Found")
+ void testCreateShortLinkUserNotFoundController() {
+ UrlRequestDto urlRequestDto = new UrlRequestDto();
+ urlRequestDto.setUrl("https://example.com/some/long/url");
+
+
+ when(linkService.createLink(Mockito.any(UrlRequestDto.class)))
+ .thenThrow(new NoSuchUserFoundInTheSystemException("User not found", "some-user-id"));
+
+ NoSuchUserFoundInTheSystemException ex = assertThrows(
+ NoSuchUserFoundInTheSystemException.class,
+ () -> linkController.createLink(urlRequestDto)
+ );
+
+ assertEquals("User not found", ex.getMessage());
+
+ verify(linkService).createLink(urlRequestDto);
+ }
+
+
+
+ @Test
+ @DisplayName("Get All My Links - Success")
+ void getAllMyLinksSuccess() {
+ 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 getAllMyActiveLinksSuccess() {
+ 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 testDeleteLinkSuccess() {
+
+ UUID linkId = UUID.randomUUID();
+ String mockCode = "abc123";
+
+ ResponseEntity mockResponse = ResponseEntity.noContent().build();
+
+ when(linkService.deleteLink(linkId)).thenReturn(mockCode);
+
+ ResponseEntity response = linkController.deleteLink(linkId);
+
+ assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode());
+ assertNull(response.getBody());
+
+ 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/exceptions/GlobalExceptionHandlerTest.java b/src/test/java/org/decepticons/linkshortener/api/exceptions/GlobalExceptionHandlerTest.java
index b44d524..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,6 +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.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;
@@ -11,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
@@ -23,187 +30,159 @@
@DisplayName("Global Exception Handler Tests")
class GlobalExceptionHandlerTest {
- private MockMvc mockMvc;
-
- @BeforeEach
- void setup() {
- // Given
- mockMvc = MockMvcBuilders
- .standaloneSetup(new TestController())
- .setControllerAdvice(new GlobalExceptionHandler())
- .build();
+ private MockMvc mockMvc;
+
+ @BeforeEach
+ void setup() {
+ // Given
+ mockMvc = MockMvcBuilders
+ .standaloneSetup(new TestController())
+ .setControllerAdvice(new GlobalExceptionHandlerController())
+ .build();
+ }
+
+ @Test
+ @DisplayName("given UserAlreadyExistsException, when thrown, then returns 409 CONFLICT")
+ void givenUserAlreadyExistsException_whenThrown_thenReturnConflict() throws Exception {
+ // Given
+ String expectedMessage = "User with username 'testuser' already exists";
+ String expectedError = "User Already Exists";
+ int expectedStatus = HttpStatus.CONFLICT.value();
+
+ // When & Then
+ mockMvc.perform(get("/test-user-exists"))
+ .andExpect(status().isConflict())
+ .andExpect(jsonPath("$.status").value(expectedStatus))
+ .andExpect(jsonPath("$.error").value(expectedError))
+ .andExpect(jsonPath("$.message").value(expectedMessage));
+ }
+
+ @Test
+ @DisplayName("given UserNotFoundException, when thrown, then returns 404 NOT FOUND")
+ void givenUserNotFoundException_whenThrown_thenReturnNotFound() throws Exception {
+ // Given
+ String expectedMessage = "User with username 'nonexistent' not found";
+ String expectedError = "User Not Found";
+ int expectedStatus = HttpStatus.NOT_FOUND.value();
+
+ // When & Then
+ mockMvc.perform(get("/test-user-not-found"))
+ .andExpect(status().isNotFound())
+ .andExpect(jsonPath("$.status").value(expectedStatus))
+ .andExpect(jsonPath("$.error").value(expectedError))
+ .andExpect(jsonPath("$.message").value(expectedMessage));
+ }
+
+ @Test
+ @DisplayName("given InvalidPasswordException, when thrown, then returns 400 BAD REQUEST")
+ void givenInvalidPasswordException_whenThrown_thenReturnBadRequest() throws Exception {
+ // Given
+ String expectedMessage = "Password does not meet complexity requirements";
+ String expectedError = "Invalid Password";
+ int expectedStatus = HttpStatus.BAD_REQUEST.value();
+
+ // When & Then
+ mockMvc.perform(get("/test-invalid-password"))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.status").value(expectedStatus))
+ .andExpect(jsonPath("$.error").value(expectedError))
+ .andExpect(jsonPath("$.message").value(expectedMessage));
+ }
+
+ @Test
+ @DisplayName("given InvalidTokenException, when thrown, then returns 401 UNAUTHORIZED")
+ void givenInvalidTokenException_whenThrown_thenReturnUnauthorized() throws Exception {
+ // Given
+ String expectedMessage = "Invalid Token";
+ String expectedError = "Invalid Token";
+ int expectedStatus = HttpStatus.UNAUTHORIZED.value();
+
+ // When & Then
+ mockMvc.perform(get("/test-invalid-token"))
+ .andExpect(status().isUnauthorized())
+ .andExpect(jsonPath("$.status").value(expectedStatus))
+ .andExpect(jsonPath("$.error").value(expectedError))
+ .andExpect(jsonPath("$.message").value(expectedMessage));
+ }
+
+ @Test
+ @DisplayName("given ExpiredTokenException, when thrown, then returns 401 UNAUTHORIZED")
+ void givenExpiredTokenException_whenThrown_thenReturnUnauthorized() throws Exception {
+ // Given
+ String expectedMessage = "Expired token";
+ String expectedError = "Expired Token";
+ int expectedStatus = HttpStatus.UNAUTHORIZED.value();
+
+ // When & Then
+ mockMvc.perform(get("/test-expired-token"))
+ .andExpect(status().isUnauthorized())
+ .andExpect(jsonPath("$.status").value(expectedStatus))
+ .andExpect(jsonPath("$.error").value(expectedError))
+ .andExpect(jsonPath("$.message").value(expectedMessage));
+ }
+
+ @Test
+ @DisplayName("given a generic Exception, when thrown, then returns 500 INTERNAL SERVER ERROR")
+ void givenGenericException_whenThrown_thenReturnInternalServerError() throws Exception {
+ // Given
+ String expectedMessage = "Something went wrong";
+ String expectedError = "Server Error";
+ int expectedStatus = HttpStatus.INTERNAL_SERVER_ERROR.value();
+
+ // When & Then
+ mockMvc.perform(get("/test-generic-exception"))
+ .andExpect(status().isInternalServerError())
+ .andExpect(jsonPath("$.status").value(expectedStatus))
+ .andExpect(jsonPath("$.error").value(expectedError))
+ .andExpect(jsonPath("$.message").value(expectedMessage));
+ }
+
+ /**
+ * A simple dummy controller to trigger the exceptions for testing.
+ */
+ @RestController
+ private static class TestController {
+
+
+ @GetMapping("/test-authentication-exception")
+ public void testAuthenticationException() {
+ throw new BadCredentialsException("Invalid username or password");
}
- @Test
- @DisplayName("given AuthenticationException (BadCredentialsException), when thrown, then returns 401 UNAUTHORIZED")
- void givenAuthenticationException_whenThrown_thenReturnUnauthorized() throws Exception {
- // Given
- String expectedError = "Authentication Failed";
- int expectedStatus = HttpStatus.UNAUTHORIZED.value();
-
- // When & Then
- mockMvc.perform(get("/test-authentication-exception"))
- .andExpect(status().isUnauthorized())
- .andExpect(jsonPath("$.status").value(expectedStatus))
- .andExpect(jsonPath("$.error").value(expectedError));
+ @GetMapping("/test-jwt-exception")
+ public void testJwtException() {
+ throw new JwtException("JWT expired");
}
- @Test
- @DisplayName("given JwtException, when thrown, then returns 401 UNAUTHORIZED")
- void givenJwtException_whenThrown_thenReturnUnauthorized() throws Exception {
- // Given
- String expectedError = "Invalid Token";
- String expectedMessage = "JWT expired";
- int expectedStatus = HttpStatus.UNAUTHORIZED.value();
-
- // When & Then
- mockMvc.perform(get("/test-jwt-exception"))
- .andExpect(status().isUnauthorized())
- .andExpect(jsonPath("$.status").value(expectedStatus))
- .andExpect(jsonPath("$.error").value(expectedError))
- .andExpect(jsonPath("$.message").value(expectedMessage));
+ @GetMapping("/test-user-exists")
+ public void testUserExists() {
+ throw new UserAlreadyExistsException("testuser");
}
- @Test
- @DisplayName("given UserAlreadyExistsException, when thrown, then returns 409 CONFLICT")
- void givenUserAlreadyExistsException_whenThrown_thenReturnConflict() throws Exception {
- // Given
- String expectedMessage = "User with username 'testuser' already exists";
- String expectedError = "User Already Exists";
- int expectedStatus = HttpStatus.CONFLICT.value();
-
- // When & Then
- mockMvc.perform(get("/test-user-exists"))
- .andExpect(status().isConflict())
- .andExpect(jsonPath("$.status").value(expectedStatus))
- .andExpect(jsonPath("$.error").value(expectedError))
- .andExpect(jsonPath("$.message").value(expectedMessage));
- }
-
- @Test
- @DisplayName("given UserNotFoundException, when thrown, then returns 404 NOT FOUND")
- void givenUserNotFoundException_whenThrown_thenReturnNotFound() throws Exception {
- // Given
- String expectedMessage = "User with username 'nonexistent' not found";
- String expectedError = "User Not Found";
- int expectedStatus = HttpStatus.NOT_FOUND.value();
-
- // When & Then
- mockMvc.perform(get("/test-user-not-found"))
- .andExpect(status().isNotFound())
- .andExpect(jsonPath("$.status").value(expectedStatus))
- .andExpect(jsonPath("$.error").value(expectedError))
- .andExpect(jsonPath("$.message").value(expectedMessage));
- }
- @Test
- @DisplayName("given InvalidPasswordException, when thrown, then returns 400 BAD REQUEST")
- void givenInvalidPasswordException_whenThrown_thenReturnBadRequest() throws Exception {
- // Given
- String expectedMessage = "Password does not meet complexity requirements";
- String expectedError = "Invalid Password";
- int expectedStatus = HttpStatus.BAD_REQUEST.value();
-
- // When & Then
- mockMvc.perform(get("/test-invalid-password"))
- .andExpect(status().isBadRequest())
- .andExpect(jsonPath("$.status").value(expectedStatus))
- .andExpect(jsonPath("$.error").value(expectedError))
- .andExpect(jsonPath("$.message").value(expectedMessage));
+ @GetMapping("/test-user-not-found")
+ public void testUserNotFound() {
+ throw new UserNotFoundException("nonexistent");
}
- @Test
- @DisplayName("given InvalidTokenException, when thrown, then returns 401 UNAUTHORIZED")
- void givenInvalidTokenException_whenThrown_thenReturnUnauthorized() throws Exception {
- // Given
- String expectedMessage = "Invalid Token";
- String expectedError = "Invalid Token";
- int expectedStatus = HttpStatus.UNAUTHORIZED.value();
-
- // When & Then
- mockMvc.perform(get("/test-invalid-token"))
- .andExpect(status().isUnauthorized())
- .andExpect(jsonPath("$.status").value(expectedStatus))
- .andExpect(jsonPath("$.error").value(expectedError))
- .andExpect(jsonPath("$.message").value(expectedMessage));
+ @GetMapping("/test-invalid-password")
+ public void testInvalidPassword() {
+ throw new InvalidPasswordException("Password does not meet complexity requirements");
}
- @Test
- @DisplayName("given ExpiredTokenException, when thrown, then returns 401 UNAUTHORIZED")
- void givenExpiredTokenException_whenThrown_thenReturnUnauthorized() throws Exception {
- // Given
- String expectedMessage = "Expired token";
- String expectedError = "Expired Token";
- int expectedStatus = HttpStatus.UNAUTHORIZED.value();
-
- // When & Then
- mockMvc.perform(get("/test-expired-token"))
- .andExpect(status().isUnauthorized())
- .andExpect(jsonPath("$.status").value(expectedStatus))
- .andExpect(jsonPath("$.error").value(expectedError))
- .andExpect(jsonPath("$.message").value(expectedMessage));
+ @GetMapping("/test-invalid-token")
+ public void testInvalidToken() {
+ throw new InvalidTokenException("Invalid Token");
}
- @Test
- @DisplayName("given a generic Exception, when thrown, then returns 500 INTERNAL SERVER ERROR")
- void givenGenericException_whenThrown_thenReturnInternalServerError() throws Exception {
- // Given
- String expectedMessage = "Something went wrong";
- String expectedError = "Server Error";
- int expectedStatus = HttpStatus.INTERNAL_SERVER_ERROR.value();
-
- // When & Then
- mockMvc.perform(get("/test-generic-exception"))
- .andExpect(status().isInternalServerError())
- .andExpect(jsonPath("$.status").value(expectedStatus))
- .andExpect(jsonPath("$.error").value(expectedError))
- .andExpect(jsonPath("$.message").value(expectedMessage));
+ @GetMapping("/test-expired-token")
+ public void testExpiredToken() {
+ throw new ExpiredTokenException("Expired token");
}
- /**
- * A simple dummy controller to trigger the exceptions for testing.
- */
- @RestController
- private static class TestController {
-
- @GetMapping("/test-authentication-exception")
- public void testAuthenticationException() {
- throw new BadCredentialsException("Invalid username or password");
- }
-
- @GetMapping("/test-jwt-exception")
- public void testJwtException() {
- throw new JwtException("JWT expired");
- }
-
- @GetMapping("/test-user-exists")
- public void testUserExists() {
- throw new UserAlreadyExistsException("testuser");
- }
-
- @GetMapping("/test-user-not-found")
- public void testUserNotFound() {
- throw new UserNotFoundException("nonexistent");
- }
-
- @GetMapping("/test-invalid-password")
- public void testInvalidPassword() {
- throw new InvalidPasswordException("Password does not meet complexity requirements");
- }
-
- @GetMapping("/test-invalid-token")
- public void testInvalidToken() {
- throw new InvalidTokenException("Invalid Token");
- }
-
- @GetMapping("/test-expired-token")
- public void testExpiredToken() {
- throw new ExpiredTokenException("Expired token");
- }
-
- @GetMapping("/test-generic-exception")
- public void testGenericException() throws Exception {
- throw new Exception("Something went wrong");
- }
+ @GetMapping("/test-generic-exception")
+ public void testGenericException() throws Exception {
+ throw new Exception("Something went wrong");
}
-}
+ }
+}
\ No newline at end of file
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 bde8a0a..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,10 +1,21 @@
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 org.decepticons.linkshortener.api.exceptions.InvalidTokenException;
+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;
import org.junit.jupiter.api.Test;
@@ -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 f444def..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,9 +1,22 @@
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.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.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.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;
@@ -20,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.
@@ -110,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());
}
@@ -134,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 d427044..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,8 +1,18 @@
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 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;
import org.decepticons.linkshortener.api.model.User;
import org.decepticons.linkshortener.api.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
@@ -14,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.
@@ -94,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));
}
@@ -114,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/LinkServiceMethodsTest.java b/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplMethodsTest.java
similarity index 68%
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 2105078..72edfb9 100644
--- a/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceMethodsTest.java
+++ b/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplMethodsTest.java
@@ -1,47 +1,54 @@
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;
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;
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 LinkServiceMethodsTest {
+public class LinkServiceImplMethodsTest {
@Mock
private LinkRepository linkRepository;
@Mock
- private UserRepository userRepository;
+ 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";
@@ -55,9 +62,7 @@ void setUpSecurityContext() {
User testUser = new User();
- testUser.setId(testUserId);
testUser.setUsername(testUsername);
- when(userRepository.findByUsername(testUsername)).thenReturn(Optional.of(testUser));
}
@AfterEach
@@ -67,12 +72,14 @@ void clearSecurityContext() {
@Test
@DisplayName("Get All My Links - Success")
- void getAllMyLinks_SUCCESS() {
+ void getAllMyLinksSuccess() {
Link link1 = new Link();
- link1.setOwner(userRepository.findByUsername(testUsername).get());
+ link1.setOwner(new User());
Page mockPage = new PageImpl<>(List.of(link1));
+
+ when(userServiceImpl.getCurrentUserId()).thenReturn(testUserId);
when(linkRepository.findAllByOwnerId(eq(testUserId), any(Pageable.class)))
.thenReturn(mockPage);
@@ -85,14 +92,16 @@ 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(userRepository.findByUsername(testUsername).get());
+ link1.setOwner(new User());
Page mockPage = new PageImpl<>(List.of(link1));
- when(linkRepository.findAllByOwnerIdAndStatus(eq(testUserId), eq(LinkStatus.ACTIVE), any(Pageable.class)))
+ when(userServiceImpl.getCurrentUserId()).thenReturn(testUserId);
+ when(linkRepository.findAllByOwnerIdAndStatus(eq(testUserId),
+ eq(LinkStatus.ACTIVE), any(Pageable.class)))
.thenReturn(mockPage);
Page result = linkService.getAllMyActiveLinks(0, 10);
@@ -104,18 +113,21 @@ void getAllMyActiveLinks_SUCCESS() {
@Test
@DisplayName("Delete Link - Success")
- void deleteLink_SUCCESS() {
- UUID linkId = UUID.randomUUID();
+ void deleteLinkSuccess() {
+ User owner = new User();
+ ReflectionTestUtils.setField(owner, "id", testUserId);
+ owner.setUsername(testUsername);
Link link = new Link();
link.setCode("abc123");
- link.setOwner(userRepository.findByUsername(testUsername).get());
+ link.setOwner(owner);
+ UUID linkId = UUID.randomUUID();
+ 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 63%
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 d285813..e6b5956 100644
--- a/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceTest.java
+++ b/src/test/java/org/decepticons/linkshortener/api/service/LinkServiceImplTest.java
@@ -1,69 +1,70 @@
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;
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;
+import org.springframework.test.util.ReflectionTestUtils;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-import java.util.List;
-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.mock;
-import static org.mockito.Mockito.when;
-
@ExtendWith(org.mockito.junit.jupiter.MockitoExtension.class)
-class LinkServiceTest {
+class LinkServiceImplTest {
@Mock
private LinkRepository linkRepository;
- @InjectMocks
- private LinkService linkService;
+
@Mock
- private UserRepository userRepository;
+ private UserServiceImpl userServiceImpl;
+
+ @InjectMocks
+ private LinkServiceImpl linkServiceImpl;
+
- @BeforeEach
- void setUp() {
- linkRepository = mock(LinkRepository.class);
- linkService = new LinkService(linkRepository, null, null);
- }
@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");
-
+ 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());
@@ -73,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",
@@ -84,26 +90,19 @@ 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));
- when(linkRepository.save(any(Link.class))).thenAnswer(i -> i.getArguments()[0]);
+ LinkResponseDto result = linkServiceImpl.incrementClicks(linkResponseDto);
+ assertNotEquals(1, result.clicks());
-
- linkService.incrementClicks(linkResponseDto);
-
- assertNotEquals(0, link.getClicks());
}
@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");
@@ -113,7 +112,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());
@@ -127,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);
@@ -143,7 +141,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());
@@ -156,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",
@@ -170,16 +174,10 @@ 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]);
- LinkResponseDto result = linkService.deactivateLink(linkResponseDto);
+ LinkResponseDto result = linkServiceImpl.deactivateLink(linkResponseDto);
assertEquals("INACTIVE", result.status());
}
@@ -187,7 +185,7 @@ void deactivateLink_Success() {
@Test
@DisplayName("Validate Link - Success")
- void validateLink_Success() {
+ void validateLinkSuccess() {
LinkResponseDto linkResponseDto = new LinkResponseDto(
UUID.randomUUID(),
"abc123",
@@ -199,8 +197,41 @@ void validateLink_Success() {
UUID.randomUUID()
);
- linkService.validateLink(linkResponseDto);
- Assertions.assertTrue(linkService.validateLink(linkResponseDto));
+ linkServiceImpl.validateLink(linkResponseDto);
+ Assertions.assertTrue(linkServiceImpl.validateLink(linkResponseDto));
+
+ }
+
+ @Test
+ @DisplayName("Update Link Expiration - Success")
+ void testUpdateLinkExpiration() {
+ String code = "abc123";
+ User owner = new User();
+ owner.setUsername("testUser");
+ ReflectionTestUtils.setField(owner, "id", 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);
+ SecurityContextHolder.setContext(securityContext);
+
+ when(linkRepository.findByCode(code)).thenReturn(Optional.of(link));
+ when(linkRepository.save(any(Link.class))).thenAnswer(i -> i.getArguments()[0]);
+ when(userServiceImpl.getCurrentUserId()).thenReturn(owner.getId());
+
+ LinkResponseDto response = linkServiceImpl.updateLinkExpiration(code, Instant
+ .now()
+ .plusSeconds(7200));
+
+ assertNotNull(response);
+ assertEquals(code, response.code());
+ assertEquals(link.getExpiresAt(), response.expiresAt());
+ verify(linkRepository, times(1)).findByCode(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
new file mode 100644
index 0000000..5003d0f
--- /dev/null
+++ b/src/test/java/org/decepticons/linkshortener/api/service/UserServiceImplTest.java
@@ -0,0 +1,102 @@
+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;
+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.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+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
+ 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 testGetCurrentUserSuccess() {
+ User fakeUser = new User();
+ 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 testGetCurrentUserIdSuccess() {
+ UUID fakeId = UUID.randomUUID();
+ User fakeUser = new User();
+ ReflectionTestUtils.setField(fakeUser, "id", 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 testGetCurrentUserUserNotFound() {
+ 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/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 45e2747..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,9 +1,21 @@
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.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,18 +23,11 @@
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;
-import java.time.Instant;
-import java.util.Collections;
-import java.util.UUID;
-
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
-
@SpringBootTest
@AutoConfigureMockMvc
class LinkCrudControllerTest {
@@ -31,7 +36,7 @@ class LinkCrudControllerTest {
private MockMvc mockMvc;
@MockitoBean
- private LinkService linkService;
+ private LinkServiceImpl linkServiceImpl;
@Test
@WithMockUser(username = "testuser", roles = {"USER"})
@@ -53,7 +58,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 +90,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 +105,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);
}
}
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 5f745ee..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,18 +1,25 @@
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.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 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 {
@@ -20,15 +27,15 @@ class RedirectControllerTest {
@InjectMocks
private RedirectController redirectController;
@Mock
- private LinkService linkService;
+ private LinkServiceImpl linkServiceImpl;
@Mock
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(),
@@ -42,23 +49,23 @@ 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
- 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(),
@@ -71,19 +78,19 @@ 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;
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);
}
}