diff --git a/src/main/java/org/openpodcastapi/opa/helpers/UUIDHelper.java b/src/main/java/org/openpodcastapi/opa/helpers/UUIDHelper.java index db3255f..37bd6b0 100644 --- a/src/main/java/org/openpodcastapi/opa/helpers/UUIDHelper.java +++ b/src/main/java/org/openpodcastapi/opa/helpers/UUIDHelper.java @@ -22,7 +22,22 @@ private UUIDHelper() { /// @param feedUrl the URL to sanitize /// @return the sanitized URL public static String sanitizeFeedUrl(String feedUrl) { - return feedUrl.replaceFirst("^[a-zA-Z]+://", "").replaceAll("/+$", ""); + if (feedUrl == null || feedUrl.isBlank()) { + throw new IllegalArgumentException("Invalid feed URL passed to function"); + } + + // Reject unsupported schemes (e.g., ftp://) + if (feedUrl.matches("^[a-zA-Z]+://.*") && !feedUrl.startsWith("http://") && !feedUrl.startsWith("https://")) { + throw new IllegalArgumentException("Invalid feed URL passed to function"); + } + + String sanitized = feedUrl.replaceFirst("^(https?://)", "").replaceAll("/+$", ""); + + if (!sanitized.contains(".")) { + throw new IllegalArgumentException("Invalid feed URL passed to function"); + } + + return sanitized; } /// Calculates the UUID of a provided feed URL using Podcast index methodology. @@ -34,8 +49,7 @@ public static String sanitizeFeedUrl(String feedUrl) { /// @return the calculated UUID public static UUID getFeedUUID(String feedUrl) { final String sanitizedFeedUrl = sanitizeFeedUrl(feedUrl); - final UUID feedUUID = UUID.fromString(sanitizedFeedUrl); - return generator.generate(feedUUID.toString()); + return generator.generate(sanitizedFeedUrl); } /// Validates that a supplied subscription UUID has been calculated properly diff --git a/src/main/java/org/openpodcastapi/opa/subscription/model/Subscription.java b/src/main/java/org/openpodcastapi/opa/subscription/model/Subscription.java index 595f8a6..d04a5c2 100644 --- a/src/main/java/org/openpodcastapi/opa/subscription/model/Subscription.java +++ b/src/main/java/org/openpodcastapi/opa/subscription/model/Subscription.java @@ -1,18 +1,17 @@ package org.openpodcastapi.opa.subscription.model; import jakarta.persistence.*; -import lombok.Generated; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import java.time.Instant; import java.util.Set; import java.util.UUID; @Entity -@Table(name = "subscriptions") @NoArgsConstructor +@AllArgsConstructor +@Builder +@Table(name = "subscriptions") public class Subscription { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/org/openpodcastapi/opa/subscription/model/UserSubscription.java b/src/main/java/org/openpodcastapi/opa/subscription/model/UserSubscription.java index 9ec9766..1994b7c 100644 --- a/src/main/java/org/openpodcastapi/opa/subscription/model/UserSubscription.java +++ b/src/main/java/org/openpodcastapi/opa/subscription/model/UserSubscription.java @@ -1,10 +1,7 @@ package org.openpodcastapi.opa.subscription.model; import jakarta.persistence.*; -import lombok.Generated; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import org.openpodcastapi.opa.user.model.User; import java.time.Instant; @@ -12,6 +9,8 @@ @Entity @NoArgsConstructor +@AllArgsConstructor +@Builder @Table(name = "user_subscription") public class UserSubscription { @Id diff --git a/src/test/java/org/openpodcastapi/opa/helpers/UUIDHelperTest.java b/src/test/java/org/openpodcastapi/opa/helpers/UUIDHelperTest.java new file mode 100644 index 0000000..aab7a51 --- /dev/null +++ b/src/test/java/org/openpodcastapi/opa/helpers/UUIDHelperTest.java @@ -0,0 +1,88 @@ +package org.openpodcastapi.opa.helpers; + +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.openpodcastapi.opa.helpers.UUIDHelper.*; + +class UUIDHelperTest { + @Test + void sanitizeFeedUrl_shouldSanitizeValidUrl() { + final String feedUrl = "https://test.com/feed1/"; + final String expectedUrl = "test.com/feed1"; + String cleanedUrl = sanitizeFeedUrl(feedUrl); + + assertEquals(expectedUrl, cleanedUrl); + } + + @Test + void sanitizeFeedUrl_shouldSanitizeUrlWithoutScheme() { + final String feedUrl = "test.com/feed1"; + final String expectedUrl = "test.com/feed1"; + String cleanedUrl = sanitizeFeedUrl(feedUrl); + + assertEquals(expectedUrl, cleanedUrl); + } + + @Test + void sanitizeFeedUrl_shouldThrowOnInvalidUrl() { + final String feedUrl = "ftp://test.com/feed1"; + final String expectedMessage = "Invalid feed URL passed to function"; + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> sanitizeFeedUrl(feedUrl)); + + assertTrue(exception.getMessage().contains(expectedMessage)); + } + + @Test + void getFeedUUID_shouldReturnGeneratedUUID() { + final String feedUrl = "podnews.net/rss"; + final UUID expectedUUID = UUID.fromString("9b024349-ccf0-5f69-a609-6b82873eab3c"); + + UUID calculatedUUID = getFeedUUID(feedUrl); + + assertEquals(expectedUUID, calculatedUUID); + } + + @Test + void getFeedUUID_shouldReturnDeterministicUUID() { + final String feedUrl = "podnews.net/rss"; + final UUID incorrectUUID = UUID.fromString("d5d5520d-81da-474e-928b-5fa66233a1ac"); + + UUID calculatedUUID = getFeedUUID(feedUrl); + + assertNotEquals(incorrectUUID, calculatedUUID); + } + + @Test + void validateSubscriptionUUID_shouldReturnTrueWhenValid() { + final String feedUrl = "podnews.net/rss"; + final UUID expectedUUID = UUID.fromString("9b024349-ccf0-5f69-a609-6b82873eab3c"); + + assertTrue(validateSubscriptionUUID(feedUrl, expectedUUID)); + } + + @Test + void validateSubscriptionUUID_shouldReturnFalseWhenInvalid() { + final String feedUrl = "podnews.net/rss"; + final UUID incorrectUUID = UUID.fromString("d5d5520d-81da-474e-928b-5fa66233a1ac"); + + assertFalse(validateSubscriptionUUID(feedUrl, incorrectUUID)); + } + + @Test + void validateUUIDString_shouldReturnTrueWhenValid() { + final String validUUID = "d5d5520d-81da-474e-928b-5fa66233a1ac"; + + assertTrue(validateUUIDString(validUUID)); + } + + @Test + void validateUUIDString_shouldReturnFalseWhenInvalid() { + final String validUUID = "not-a-uuid"; + + assertFalse(validateUUIDString(validUUID)); + } +} diff --git a/src/test/java/org/openpodcastapi/opa/subscriptions/UserSubscriptionMapperTest.java b/src/test/java/org/openpodcastapi/opa/subscriptions/UserSubscriptionMapperTest.java new file mode 100644 index 0000000..30baf63 --- /dev/null +++ b/src/test/java/org/openpodcastapi/opa/subscriptions/UserSubscriptionMapperTest.java @@ -0,0 +1,70 @@ +package org.openpodcastapi.opa.subscriptions; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.openpodcastapi.opa.subscription.dto.UserSubscriptionDto; +import org.openpodcastapi.opa.subscription.mapper.UserSubscriptionMapper; +import org.openpodcastapi.opa.subscription.mapper.UserSubscriptionMapperImpl; +import org.openpodcastapi.opa.subscription.model.Subscription; +import org.openpodcastapi.opa.subscription.model.UserSubscription; +import org.openpodcastapi.opa.subscription.repository.UserSubscriptionRepository; +import org.openpodcastapi.opa.user.model.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.time.Instant; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = UserSubscriptionMapperImpl.class) +class UserSubscriptionMapperTest { + @Autowired + private UserSubscriptionMapper mapper; + + @MockitoBean + private UserSubscriptionRepository userSubscriptionRepository; + + /// Tests that a [UserSubscription] entity maps to a [UserSubscriptionDto] representation + @Test + void testToDto() { + final Instant timestamp = Instant.now(); + final UUID uuid = UUID.randomUUID(); + User user = User.builder() + .uuid(UUID.randomUUID()) + .username("test") + .email("test@test.test") + .createdAt(timestamp) + .updatedAt(timestamp) + .build(); + + Subscription subscription = Subscription.builder() + .uuid(UUID.randomUUID()) + .feedUrl("test.com/feed1") + .createdAt(timestamp) + .updatedAt(timestamp) + .build(); + + UserSubscription userSubscription = UserSubscription.builder() + .uuid(uuid) + .user(user) + .subscription(subscription) + .isSubscribed(true) + .createdAt(timestamp) + .updatedAt(timestamp) + .build(); + + UserSubscriptionDto dto = mapper.toDto(userSubscription); + assertNotNull(dto); + + // The DTO should inherit the feed URL from the Subscription + assertEquals(subscription.getFeedUrl(), dto.feedUrl()); + + // The DTO should use the Subscription's UUID rather than the UserSubscription's + assertEquals(subscription.getUuid(), dto.uuid()); + assertTrue(dto.isSubscribed()); + } +} diff --git a/src/test/java/org/openpodcastapi/opa/user/UserMapperSpringTest.java b/src/test/java/org/openpodcastapi/opa/user/UserMapperSpringTest.java index 9791c3c..0c1ef0d 100644 --- a/src/test/java/org/openpodcastapi/opa/user/UserMapperSpringTest.java +++ b/src/test/java/org/openpodcastapi/opa/user/UserMapperSpringTest.java @@ -1,25 +1,33 @@ package org.openpodcastapi.opa.user; import org.junit.jupiter.api.Test; -import org.mapstruct.factory.Mappers; +import org.junit.jupiter.api.extension.ExtendWith; import org.openpodcastapi.opa.user.dto.CreateUserDto; +import org.openpodcastapi.opa.user.dto.UserDto; import org.openpodcastapi.opa.user.mapper.UserMapper; +import org.openpodcastapi.opa.user.mapper.UserMapperImpl; import org.openpodcastapi.opa.user.model.User; import org.openpodcastapi.opa.user.repository.UserRepository; -import org.openpodcastapi.opa.user.dto.UserDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; import java.time.Instant; import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = UserMapperImpl.class) class UserMapperSpringTest { - private final UserMapper mapper = Mappers.getMapper(UserMapper.class); + @Autowired + private UserMapper mapper; @MockitoBean private UserRepository userRepository; + /// Tests that a [User] entity maps to a [UserDto] representation @Test void testToDto() { final Instant timestamp = Instant.now(); @@ -41,6 +49,7 @@ void testToDto() { assertEquals(user.getUpdatedAt(), dto.updatedAt()); } + /// Tests that a [CreateUserDto] maps to a [User] entity @Test void testToEntity() { CreateUserDto dto = new CreateUserDto("test", "testPassword", "test@test.test");