diff --git a/pom.xml b/pom.xml
index 2adffac..1143015 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,115 +1,132 @@
- 4.0.0
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ 4.0.0
- com.dmdev
- junit5-trainer
- 1.0-SNAPSHOT
+ com.dmdev
+ junit5-trainer
+ 1.0-SNAPSHOT
-
- UTF-8
- 17
- 17
- 1.18.22
-
+
+ UTF-8
+ 17
+ 17
+ 1.18.22
+
-
-
- org.apache.commons
- commons-lang3
- 3.12.0
-
+
+
+ org.apache.commons
+ commons-lang3
+ 3.12.0
+
-
- org.postgresql
- postgresql
- 42.3.3
- runtime
-
+
+ org.postgresql
+ postgresql
+ 42.7.2
+ runtime
+
-
- org.projectlombok
- lombok
- ${lombok.version}
- provided
-
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+ provided
+
-
- org.junit.jupiter
- junit-jupiter-engine
- 5.8.2
- test
-
-
- com.h2database
- h2
- 2.1.210
- test
-
-
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.11.4
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ 5.11.3
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.26.3
+
+
+ org.mockito
+ mockito-junit-jupiter
+ 5.14.0
+ test
+
+
+ com.h2database
+ h2
+ 2.2.220
+ test
+
+
-
-
-
- org.apache.maven.plugins
- maven-wrapper-plugin
- 3.1.0
-
-
- org.apache.maven.plugins
- maven-failsafe-plugin
- 3.0.0-M5
-
-
-
- integration-test
- verify
-
-
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
- 3.10.1
-
-
-
- org.projectlombok
- lombok
- ${lombok.version}
-
-
-
-
-
- org.apache.maven.plugins
- maven-surefire-plugin
- 3.0.0-M5
-
-
- org.jacoco
- jacoco-maven-plugin
- 0.8.7
-
-
- prepare-agent
-
- prepare-agent
-
-
-
- generate-jacoco-report
-
- report
-
- verify
-
-
-
-
-
+
+
+
+ org.apache.maven.plugins
+ maven-wrapper-plugin
+ 3.1.0
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+ 3.0.0-M5
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.10.1
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.0.0-M5
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.7
+
+
+ prepare-agent
+
+ prepare-agent
+
+
+
+ generate-jacoco-report
+
+ report
+
+ verify
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/src/test/java/com/dmdev/dao/SubscriptionDaoIT.java b/src/test/java/com/dmdev/dao/SubscriptionDaoIT.java
new file mode 100644
index 0000000..91d545f
--- /dev/null
+++ b/src/test/java/com/dmdev/dao/SubscriptionDaoIT.java
@@ -0,0 +1,142 @@
+package com.dmdev.dao;
+
+import static org.assertj.core.api.Assertions.assertThat;
+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 com.dmdev.entity.Provider;
+import com.dmdev.entity.Status;
+import com.dmdev.entity.Subscription;
+import com.dmdev.integration.IntegrationTestBase;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import org.junit.jupiter.api.Test;
+
+class SubscriptionDaoIT extends IntegrationTestBase {
+
+ private final SubscriptionDao subscriptionDao = SubscriptionDao.getInstance();
+
+ @Test
+ void findAll() {
+ var subscriptionFirst = subscriptionDao.insert(getSubscription("first"));
+ var subscriptionSecond = subscriptionDao.insert(getSubscription("second "));
+ var subscriptionThird = subscriptionDao.insert(getSubscription("third"));
+
+ List result = subscriptionDao.findAll();
+
+ assertEquals(List.of(subscriptionFirst, subscriptionSecond, subscriptionThird), result);
+
+ List ids = result.stream()
+ .map(Subscription::getId)
+ .toList();
+ assertThat(ids).contains(subscriptionFirst.getId(), subscriptionSecond.getId(),
+ subscriptionThird.getId());
+ }
+
+
+ @Test
+ void findByIdWhenIdExistsThenReturnSubscription() {
+ var subscriptionFirst = subscriptionDao.insert(getSubscription("first"));
+
+ var result = subscriptionDao.findById(subscriptionFirst.getId());
+
+ assertNotNull(result);
+ assertEquals(Optional.of(subscriptionFirst), result);
+ }
+
+ @Test
+ void findByIdWhenIdNotExistsThenReturnOptionalEmpty() {
+ var result = subscriptionDao.findById(3212);
+
+ assertEquals(Optional.empty(), result);
+ }
+
+ @Test
+ void deleteWhenIdExistsThenReturnTrue() {
+ var subscriptionFirst = subscriptionDao.insert(getSubscription("first"));
+
+ var result = subscriptionDao.delete(subscriptionFirst.getId());
+
+ assertTrue(result);
+
+ }
+
+ @Test
+ void deleteWhenIdNotExistsThenReturnFalse() {
+ subscriptionDao.insert(getSubscription("first"));
+
+ var result = subscriptionDao.delete(1234);
+
+ assertFalse(result);
+
+ }
+
+ @Test
+ void update() {
+ var subscription = getSubscription("first");
+ subscriptionDao.insert(subscription);
+ subscription.setName("second");
+ subscription.setProvider(Provider.GOOGLE);
+
+ var result = subscriptionDao.update(subscription);
+
+ assertNotNull(result);
+ assertEquals(subscription, result);
+
+ }
+
+ @Test
+ void insert() {
+ var subscription = getSubscription("first");
+
+ var result = subscriptionDao.insert(subscription);
+
+ assertNotNull(result);
+ }
+
+ @Test
+ void findByUserIdWhenIdExistsThenReturnSubscriptions() {
+ var userid = 123;
+ var subscriptionFirst = subscriptionDao.insert(getSubscription("first").setUserId(userid));
+ var subscriptionSecond = subscriptionDao.insert(getSubscription("second").setUserId(userid));
+
+ List result = subscriptionDao.findByUserId(userid);
+
+ assertNotNull(result);
+ assertEquals(2, result.size());
+
+ List ids = result.stream()
+ .map(Subscription::getId)
+ .toList();
+ assertThat(ids).contains(subscriptionFirst.getId(), subscriptionSecond.getId());
+
+ }
+
+ @Test
+ void findByUserIdWhenIdNotExistsThenReturnEmptyList() {
+ var userid = 123;
+ subscriptionDao.insert(getSubscription("first").setUserId(userid));
+ subscriptionDao.insert(getSubscription("second").setUserId(userid));
+
+ var result = subscriptionDao.findByUserId(3212);
+
+ assertEquals(Collections.EMPTY_LIST, result);
+ }
+
+
+ private Subscription getSubscription(String name) {
+ return Subscription.builder()
+ .userId(123)
+ .name(name)
+ .provider(Provider.APPLE)
+ .status(Status.ACTIVE)
+ .expirationDate(Instant.parse("2025-11-12T10:00:00Z"))
+ .build();
+ }
+
+
+}
diff --git a/src/test/java/com/dmdev/mapper/CreateSubscriptionMapperTest.java b/src/test/java/com/dmdev/mapper/CreateSubscriptionMapperTest.java
new file mode 100644
index 0000000..fe9e9e2
--- /dev/null
+++ b/src/test/java/com/dmdev/mapper/CreateSubscriptionMapperTest.java
@@ -0,0 +1,46 @@
+package com.dmdev.mapper;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import com.dmdev.dto.CreateSubscriptionDto;
+import com.dmdev.entity.Provider;
+import com.dmdev.entity.Status;
+import com.dmdev.entity.Subscription;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneOffset;
+import org.junit.jupiter.api.Test;
+
+class CreateSubscriptionMapperTest {
+
+ CreateSubscriptionMapper mapper = CreateSubscriptionMapper.getInstance();
+
+ @Test
+ void map() {
+ Instant fixedInstant = LocalDate.of(2026, 11, 12)
+ .atStartOfDay(ZoneOffset.UTC)
+ .toInstant();
+
+ CreateSubscriptionDto dto = CreateSubscriptionDto.builder()
+ .name("dummy")
+ .userId(123)
+ .expirationDate(fixedInstant)
+ .provider(Provider.APPLE.name())
+ .build();
+
+ Subscription expectedSubscription = Subscription.builder()
+ .name("dummy")
+ .userId(123)
+ .expirationDate(fixedInstant)
+ .provider(Provider.APPLE)
+ .status(Status.ACTIVE)
+ .build();
+
+ Subscription resultSubscription = mapper.map(dto);
+
+ assertNotNull(resultSubscription);
+ assertEquals(expectedSubscription, resultSubscription);
+
+ }
+}
diff --git a/src/test/java/com/dmdev/service/SubscriptionServiceTest.java b/src/test/java/com/dmdev/service/SubscriptionServiceTest.java
new file mode 100644
index 0000000..3dbeb94
--- /dev/null
+++ b/src/test/java/com/dmdev/service/SubscriptionServiceTest.java
@@ -0,0 +1,214 @@
+package com.dmdev.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.mockito.ArgumentMatchers.any;
+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 com.dmdev.dao.SubscriptionDao;
+import com.dmdev.dto.CreateSubscriptionDto;
+import com.dmdev.entity.Provider;
+import com.dmdev.entity.Status;
+import com.dmdev.entity.Subscription;
+import com.dmdev.exception.SubscriptionException;
+import com.dmdev.exception.ValidationException;
+import com.dmdev.mapper.CreateSubscriptionMapper;
+import com.dmdev.validator.CreateSubscriptionValidator;
+import com.dmdev.validator.Error;
+import com.dmdev.validator.ValidationResult;
+import java.time.Clock;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.Optional;
+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;
+
+@ExtendWith(MockitoExtension.class)
+class SubscriptionServiceTest {
+
+ @Mock
+ private SubscriptionDao subscriptionDao;
+
+ @Mock
+ private CreateSubscriptionMapper createSubscriptionMapper;
+
+ @Mock
+ private CreateSubscriptionValidator createSubscriptionValidator;
+
+ @Mock
+ private Clock clock;
+
+
+ @InjectMocks
+ private SubscriptionService subscriptionService;
+
+ @Test
+ void whenCallingUpsertMethodWithInvalidDtoThenThrowValidationException() {
+ var dto = Mockito.mock(CreateSubscriptionDto.class);
+ var error = Error.of(100, "userId is invalid");
+ var validationResult = new ValidationResult();
+ validationResult.add(error);
+
+ when(createSubscriptionValidator.validate(any())).thenReturn(validationResult);
+
+ var resultException = assertThrows(ValidationException.class,
+ () -> subscriptionService.upsert(dto));
+
+ assertEquals(1, resultException.getErrors().size());
+ assertEquals(100, resultException.getErrors().get(0).getCode());
+
+ }
+
+ @Test
+ void whenCallingUpsertMethodWithNonExistingSubscriptionThenCreateANewOne() {
+ var dto = CreateSubscriptionDto.builder()
+ .name("dummy")
+ .userId(123)
+ .provider(Provider.APPLE.name())
+ .build();
+ var newSubscription = getSubscription();
+
+ when(createSubscriptionValidator.validate(any())).thenReturn(new ValidationResult());
+ when(subscriptionDao.findByUserId(any())).thenReturn(Collections.emptyList());
+ when(createSubscriptionMapper.map(dto)).thenReturn(newSubscription);
+ when(subscriptionDao.upsert(newSubscription)).thenReturn(newSubscription);
+
+ var resultSubscription = subscriptionService.upsert(dto);
+
+ assertNotNull(resultSubscription);
+ assertEquals(newSubscription, resultSubscription);
+ verify(createSubscriptionMapper).map(dto);
+ }
+
+ @Test
+ void whenCallingUpsertMethodWithAnExistingSubscriptionThenUpdateIt() {
+ Instant expirationDate = Instant.parse("2025-11-12T10:00:00Z");
+ var dto = CreateSubscriptionDto.builder()
+ .name("dummy")
+ .userId(123)
+ .provider(Provider.APPLE.name())
+ .expirationDate(expirationDate)
+ .build();
+
+ var existingSubscription = getSubscription();
+ var expectedSubscription = getSubscription().setExpirationDate(expirationDate);
+
+ when(createSubscriptionValidator.validate(any())).thenReturn(new ValidationResult());
+ when(subscriptionDao.findByUserId(123)).thenReturn(
+ Collections.singletonList(existingSubscription));
+ when(subscriptionDao.upsert(any())).thenReturn(expectedSubscription);
+
+ var resultSubscription = subscriptionService.upsert(dto);
+
+ assertNotNull(resultSubscription);
+ assertEquals(expectedSubscription, resultSubscription);
+ verify(subscriptionDao).findByUserId(123);
+ verify(subscriptionDao).upsert(expectedSubscription);
+ verify(createSubscriptionMapper, never()).map(dto);
+
+ }
+
+ @Test
+ void happyFloWhenCallingCancelMethodItUpdatesExistingSubscription() {
+ var existingSubscription = getSubscription();
+ var updatedSubscription = getSubscription().setStatus(Status.CANCELED);
+
+ when(subscriptionDao.findById(any())).thenReturn(Optional.of(existingSubscription));
+ when(subscriptionDao.update(any())).thenReturn(updatedSubscription);
+
+ subscriptionService.cancel(any());
+
+ verify(subscriptionDao, times(1)).update(updatedSubscription);
+ verify(subscriptionDao, times(1)).findById(any());
+ }
+
+ @Test
+ void whenCallingCancelMethodWithNonExistingSubscriptionIdThenThrowIllegalArgumentException() {
+
+ when(subscriptionDao.findById(any())).thenThrow(new IllegalArgumentException());
+
+ assertThrows(IllegalArgumentException.class,
+ () -> subscriptionService.cancel(123));
+
+ verify(subscriptionDao, never()).update(any());
+ }
+
+ @Test
+ void whenCallingCancelMethodWithNonActiveSubscriptionThenThrowSubscriptionException() {
+
+ int id = 123;
+ Subscription subscription = getSubscription().setStatus(Status.EXPIRED);
+
+ when(subscriptionDao.findById(id)).thenReturn(Optional.ofNullable(subscription));
+
+ Exception resultException = assertThrows(SubscriptionException.class,
+ () -> subscriptionService.cancel(id));
+
+ assertEquals("Only active subscription 123 can be canceled", resultException.getMessage());
+ verify(subscriptionDao, never()).update(any());
+ }
+
+ @Test
+ void whenCallingExpireMethodWithNonExistingSubscriptionIdThenThrowIllegalArgumentException() {
+
+ when(subscriptionDao.findById(any())).thenThrow(new IllegalArgumentException());
+
+ assertThrows(IllegalArgumentException.class,
+ () -> subscriptionService.expire(123));
+
+ verify(subscriptionDao, never()).update(any());
+ }
+
+ @Test
+ void whenCallingExpireMethodWithExpiredSubscriptionThenThrowSubscriptionException() {
+ int id = 123;
+ Subscription subscription = Subscription.builder()
+ .id(id)
+ .status(Status.EXPIRED)
+ .build();
+
+ when(subscriptionDao.findById(id)).thenReturn(Optional.ofNullable(subscription));
+
+ Exception resultException = assertThrows(SubscriptionException.class,
+ () -> subscriptionService.expire(id));
+
+ assertEquals("Subscription 123 has already expired", resultException.getMessage());
+ verify(subscriptionDao, never()).update(any());
+ }
+
+ @Test
+ void happyFloWhenCallingExpireMethodItUpdatesExistingSubscription() {
+
+ int id = 123;
+ Instant expirationDate = Instant.parse("2025-11-12T10:00:00Z");
+ Subscription givenSubscription = getSubscription();
+ Subscription expectedSubscription = getSubscription().setStatus(Status.EXPIRED)
+ .setExpirationDate(expirationDate);
+
+ when(subscriptionDao.findById(id)).thenReturn(Optional.ofNullable(givenSubscription));
+ when(subscriptionDao.update(expectedSubscription)).thenReturn(expectedSubscription);
+ when(clock.instant()).thenReturn(expirationDate);
+
+ subscriptionService.expire(id);
+
+ verify(subscriptionDao).update(expectedSubscription);
+ verify(subscriptionDao).findById(id);
+ }
+
+ Subscription getSubscription() {
+ return Subscription.builder()
+ .name("dummy")
+ .status(Status.ACTIVE)
+ .userId(123)
+ .provider(Provider.APPLE)
+ .build();
+ }
+}
diff --git a/src/test/java/com/dmdev/util/PropertiesUtilTest.java b/src/test/java/com/dmdev/util/PropertiesUtilTest.java
new file mode 100644
index 0000000..d6a4f96
--- /dev/null
+++ b/src/test/java/com/dmdev/util/PropertiesUtilTest.java
@@ -0,0 +1,29 @@
+package com.dmdev.util;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+
+import java.util.stream.Stream;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class PropertiesUtilTest {
+
+
+ @ParameterizedTest
+ @MethodSource("getPropertyArguments")
+ void checkGet(String key, String expectedValue) {
+ String actualResult = PropertiesUtil.get(key);
+
+ assertThat(actualResult).isEqualTo(expectedValue);
+ }
+
+ static Stream getPropertyArguments() {
+
+ return Stream.of(
+ Arguments.of("db.url", "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"),
+ Arguments.of("db.user", "sa"),
+ Arguments.of("db.password", "")
+ );
+ }
+}
diff --git a/src/test/java/com/dmdev/validator/CreateSubscriptionValidatorTest.java b/src/test/java/com/dmdev/validator/CreateSubscriptionValidatorTest.java
new file mode 100644
index 0000000..f2dda7e
--- /dev/null
+++ b/src/test/java/com/dmdev/validator/CreateSubscriptionValidatorTest.java
@@ -0,0 +1,130 @@
+package com.dmdev.validator;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.dmdev.dto.CreateSubscriptionDto;
+import java.time.ZonedDateTime;
+import org.junit.jupiter.api.Test;
+
+class CreateSubscriptionValidatorTest {
+
+ private final CreateSubscriptionValidator validator = CreateSubscriptionValidator.getInstance();
+
+ @Test
+ void happyFlowPassesValidation() {
+ CreateSubscriptionDto dto = CreateSubscriptionDto.builder()
+ .name("dummy")
+ .userId(123)
+ .expirationDate(ZonedDateTime.now().plusDays(1).toInstant())
+ .provider("APPLE")
+ .build();
+
+ ValidationResult result = validator.validate(dto);
+
+ assertFalse(result.hasErrors());
+
+ }
+
+ @Test
+ void invalidName() {
+ CreateSubscriptionDto dto = CreateSubscriptionDto.builder()
+ .name("")
+ .userId(123)
+ .expirationDate(ZonedDateTime.now().plusDays(1).toInstant())
+ .provider("APPLE")
+ .build();
+
+ ValidationResult result = validator.validate(dto);
+
+ assertTrue(result.hasErrors());
+ assertEquals(1, result.getErrors().size());
+ assertEquals("name is invalid", result.getErrors().get(0).getMessage());
+
+ }
+
+ @Test
+ void invalidUserId() {
+ CreateSubscriptionDto dto = CreateSubscriptionDto.builder()
+ .name("dummy")
+ .expirationDate(ZonedDateTime.now().plusDays(1).toInstant())
+ .provider("APPLE")
+ .build();
+
+ ValidationResult result = validator.validate(dto);
+
+ assertTrue(result.hasErrors());
+ assertEquals(1, result.getErrors().size());
+ assertEquals("userId is invalid", result.getErrors().get(0).getMessage());
+
+ }
+
+ @Test
+ void invalidProvider() {
+ CreateSubscriptionDto dto = CreateSubscriptionDto.builder()
+ .name("dummy")
+ .userId(123)
+ .expirationDate(ZonedDateTime.now().plusDays(1).toInstant())
+ .provider("PLUM")
+ .build();
+
+ ValidationResult result = validator.validate(dto);
+
+ assertTrue(result.hasErrors());
+ assertEquals(1, result.getErrors().size());
+ assertEquals("provider is invalid", result.getErrors().get(0).getMessage());
+
+ }
+
+ @Test
+ void invalidExpirationDate() {
+ CreateSubscriptionDto dto = CreateSubscriptionDto.builder()
+ .name("dummy")
+ .userId(123)
+ .expirationDate(ZonedDateTime.now().minusDays(1L).toInstant())
+ .provider("APPLE")
+ .build();
+
+ ValidationResult result = validator.validate(dto);
+
+ assertTrue(result.hasErrors());
+ assertEquals(1, result.getErrors().size());
+ assertEquals("expirationDate is invalid", result.getErrors().get(0).getMessage());
+
+ }
+
+ @Test
+ void expirationDateIsNull() {
+ CreateSubscriptionDto dto = CreateSubscriptionDto.builder()
+ .name("dummy")
+ .userId(123)
+ .provider("APPLE")
+ .build();
+
+ ValidationResult result = validator.validate(dto);
+
+ assertTrue(result.hasErrors());
+ assertEquals(1, result.getErrors().size());
+ assertEquals("expirationDate is invalid", result.getErrors().get(0).getMessage());
+
+ }
+
+ @Test
+ void invalidExpirationDateAndName() {
+ CreateSubscriptionDto dto = CreateSubscriptionDto.builder()
+ .name("")
+ .userId(123)
+ .expirationDate(ZonedDateTime.now().minusDays(1).toInstant())
+ .provider("APPLE")
+ .build();
+
+ ValidationResult result = validator.validate(dto);
+
+ assertTrue(result.hasErrors());
+ assertEquals(2, result.getErrors().size());
+ assertEquals("name is invalid", result.getErrors().get(0).getMessage());
+ assertEquals("expirationDate is invalid", result.getErrors().get(1).getMessage());
+
+ }
+}