diff --git a/pom.xml b/pom.xml
index 30ce822..4033412 100644
--- a/pom.xml
+++ b/pom.xml
@@ -13,9 +13,16 @@
17
17
1.18.22
+ 5.10.1
+
+ org.apache.commons
+ commons-lang3
+ 3.12.0
+
+
org.postgresql
postgresql
@@ -33,7 +40,20 @@
org.junit.jupiter
junit-jupiter-engine
- 5.8.2
+ ${junit5.version}
+ test
+
+
+
+ org.assertj
+ assertj-core
+ 3.25.2
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ ${junit5.version}
test
diff --git a/src/main/java/com/dmdev/dao/Dao.java b/src/main/java/com/dmdev/dao/Dao.java
index 433ef4a..fcacd86 100644
--- a/src/main/java/com/dmdev/dao/Dao.java
+++ b/src/main/java/com/dmdev/dao/Dao.java
@@ -1,9 +1,17 @@
package com.dmdev.dao;
+import com.dmdev.entity.BaseEntity;
+
import java.util.List;
import java.util.Optional;
-public interface Dao {
+public interface Dao> {
+
+ default T upsert(T entity) {
+ return entity.getId() != null
+ ? update(entity)
+ : insert(entity);
+ }
List findAll();
@@ -11,7 +19,7 @@ public interface Dao {
boolean delete(K id);
- void update(T entity);
+ T update(T entity);
- T save(T entity);
+ T insert(T entity);
}
diff --git a/src/main/java/com/dmdev/dao/UserDao.java b/src/main/java/com/dmdev/dao/SubscriptionDao.java
similarity index 53%
rename from src/main/java/com/dmdev/dao/UserDao.java
rename to src/main/java/com/dmdev/dao/SubscriptionDao.java
index 11ad1f5..cc91b1a 100644
--- a/src/main/java/com/dmdev/dao/UserDao.java
+++ b/src/main/java/com/dmdev/dao/SubscriptionDao.java
@@ -1,77 +1,72 @@
package com.dmdev.dao;
-import com.dmdev.entity.Gender;
-import com.dmdev.entity.Role;
-import com.dmdev.entity.User;
+import com.dmdev.entity.Provider;
+import com.dmdev.entity.Status;
+import com.dmdev.entity.Subscription;
import com.dmdev.util.ConnectionManager;
-import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
-import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static java.sql.Statement.RETURN_GENERATED_KEYS;
-import static lombok.AccessLevel.PRIVATE;
-@NoArgsConstructor(access = PRIVATE)
-public class UserDao implements Dao {
+public class SubscriptionDao implements Dao {
- private static final UserDao INSTANCE = new UserDao();
+ private static final SubscriptionDao INSTANCE = new SubscriptionDao();
private static final String GET_ALL_SQL = """
SELECT
id,
+ user_id,
name,
- birthday,
- email,
- password,
- role,
- gender
- FROM users
+ provider,
+ expiration_date,
+ status
+ FROM subscription
""";
private static final String GET_BY_ID_SQL = GET_ALL_SQL + " WHERE id = ?";
- private static final String GET_BY_EMAIL_AND_PASSWORD_SQL = GET_ALL_SQL + " WHERE email = ? AND password = ?";
+ private static final String GET_BY_USER_ID_SQL = GET_ALL_SQL + " WHERE user_id = ?";
+ private static final String DELETE_BY_ID_SQL = "DELETE FROM subscription WHERE id = ?";
private static final String SAVE_SQL =
- "INSERT INTO users (name, birthday, email, password, role, gender) VALUES (?, ?, ?, ?, ?, ?)";
- private static final String DELETE_BY_ID_SQL = "DELETE FROM users WHERE id = ?";
+ "INSERT INTO subscription (user_id, name, provider, expiration_date, status) VALUES (?, ?, ?, ?, ?)";
private static final String UPDATE_BY_ID_SQL = """
- UPDATE users
- SET name = ?,
- birthday = ?,
- email = ?,
- password = ?,
- role = ?,
- gender = ?
+ UPDATE subscription
+ SET user_id = ?,
+ name = ?,
+ provider = ?,
+ expiration_date = ?,
+ status = ?
WHERE id = ?
""";
- public static UserDao getInstance() {
+ public static SubscriptionDao getInstance() {
return INSTANCE;
}
@Override
@SneakyThrows
- public List findAll() {
+ public List findAll() {
try (var connection = ConnectionManager.get();
var preparedStatement = connection.prepareStatement(GET_ALL_SQL)) {
var resultSet = preparedStatement.executeQuery();
- List users = new ArrayList<>();
+ List subscriptions = new ArrayList<>();
while (resultSet.next()) {
- users.add(buildEntity(resultSet));
+ subscriptions.add(buildEntity(resultSet));
}
- return users;
+ return subscriptions;
}
}
@Override
@SneakyThrows
- public Optional findById(Integer id) {
+ public Optional findById(Integer id) {
try (var connection = ConnectionManager.get();
var preparedStatement = connection.prepareStatement(GET_BY_ID_SQL)) {
preparedStatement.setObject(1, id);
@@ -85,76 +80,77 @@ public Optional findById(Integer id) {
@Override
@SneakyThrows
- public User save(User entity) {
+ public boolean delete(Integer id) {
try (var connection = ConnectionManager.get();
- var preparedStatement = connection.prepareStatement(SAVE_SQL, RETURN_GENERATED_KEYS)) {
- prepareStatementToUpsert(preparedStatement, entity);
-
- preparedStatement.executeUpdate();
-
- var generatedKeys = preparedStatement.getGeneratedKeys();
- generatedKeys.next();
- entity.setId(generatedKeys.getObject("id", Integer.class));
+ var preparedStatement = connection.prepareStatement(DELETE_BY_ID_SQL)) {
+ preparedStatement.setObject(1, id);
- return entity;
+ return preparedStatement.executeUpdate() > 0;
}
}
+ @Override
@SneakyThrows
- public Optional findByEmailAndPassword(String email, String password) {
+ public Subscription update(Subscription entity) {
try (var connection = ConnectionManager.get();
- var preparedStatement = connection.prepareStatement(GET_BY_EMAIL_AND_PASSWORD_SQL)) {
- preparedStatement.setString(1, email);
- preparedStatement.setString(2, password);
+ var preparedStatement = connection.prepareStatement(UPDATE_BY_ID_SQL)) {
+ prepareStatementToUpsert(preparedStatement, entity);
+ preparedStatement.setObject(6, entity.getId());
- var resultSet = preparedStatement.executeQuery();
- return resultSet.next()
- ? Optional.of(buildEntity(resultSet))
- : Optional.empty();
+ preparedStatement.executeUpdate();
+ return entity;
}
}
@Override
@SneakyThrows
- public boolean delete(Integer id) {
+ public Subscription insert(Subscription entity) {
try (var connection = ConnectionManager.get();
- var preparedStatement = connection.prepareStatement(DELETE_BY_ID_SQL)) {
- preparedStatement.setObject(1, id);
+ var preparedStatement = connection.prepareStatement(SAVE_SQL, RETURN_GENERATED_KEYS)) {
+ prepareStatementToUpsert(preparedStatement, entity);
- return preparedStatement.executeUpdate() > 0;
+ preparedStatement.executeUpdate();
+
+ var generatedKeys = preparedStatement.getGeneratedKeys();
+ generatedKeys.next();
+ entity.setId(generatedKeys.getObject("id", Integer.class));
+
+ return entity;
}
}
- @Override
@SneakyThrows
- public void update(User entity) {
+ public List findByUserId(Integer userId) {
try (var connection = ConnectionManager.get();
- var preparedStatement = connection.prepareStatement(UPDATE_BY_ID_SQL)) {
- prepareStatementToUpsert(preparedStatement, entity);
- preparedStatement.setObject(7, entity.getId());
+ var preparedStatement = connection.prepareStatement(GET_BY_USER_ID_SQL)) {
+ preparedStatement.setObject(1, userId);
- preparedStatement.executeUpdate();
+ var resultSet = preparedStatement.executeQuery();
+ List subscriptions = new ArrayList<>();
+ while (resultSet.next()) {
+ subscriptions.add(buildEntity(resultSet));
+ }
+
+ return subscriptions;
}
}
- private User buildEntity(ResultSet resultSet) throws SQLException {
- return User.builder()
+ private Subscription buildEntity(ResultSet resultSet) throws SQLException {
+ return Subscription.builder()
.id(resultSet.getObject("id", Integer.class))
+ .userId(resultSet.getObject("user_id", Integer.class))
.name(resultSet.getObject("name", String.class))
- .birthday(resultSet.getObject("birthday", Date.class).toLocalDate())
- .email(resultSet.getObject("email", String.class))
- .password(resultSet.getObject("password", String.class))
- .role(Role.find(resultSet.getObject("role", String.class)).orElse(null))
- .gender(Gender.find(resultSet.getObject("gender", String.class)).orElse(null))
+ .provider(Provider.valueOf(resultSet.getObject("provider", String.class)))
+ .expirationDate(resultSet.getObject("expiration_date", Timestamp.class).toInstant())
+ .status(Status.valueOf(resultSet.getObject("status", String.class)))
.build();
}
- private void prepareStatementToUpsert(PreparedStatement preparedStatement, User entity) throws SQLException {
- preparedStatement.setObject(1, entity.getName());
- preparedStatement.setObject(2, entity.getBirthday());
- preparedStatement.setObject(3, entity.getEmail());
- preparedStatement.setObject(4, entity.getPassword());
- preparedStatement.setObject(5, entity.getRole() != null ? entity.getRole().name() : null);
- preparedStatement.setObject(6, entity.getGender() != null ? entity.getGender().name() : null);
+ private void prepareStatementToUpsert(PreparedStatement preparedStatement, Subscription entity) throws SQLException {
+ preparedStatement.setObject(1, entity.getUserId());
+ preparedStatement.setObject(2, entity.getName());
+ preparedStatement.setObject(3, entity.getProvider().name());
+ preparedStatement.setObject(4, Timestamp.from(entity.getExpirationDate()));
+ preparedStatement.setObject(5, entity.getStatus().name());
}
}
diff --git a/src/main/java/com/dmdev/dto/CreateSubscriptionDto.java b/src/main/java/com/dmdev/dto/CreateSubscriptionDto.java
new file mode 100644
index 0000000..f752b76
--- /dev/null
+++ b/src/main/java/com/dmdev/dto/CreateSubscriptionDto.java
@@ -0,0 +1,15 @@
+package com.dmdev.dto;
+
+import lombok.Builder;
+import lombok.Value;
+
+import java.time.Instant;
+
+@Value
+@Builder
+public class CreateSubscriptionDto {
+ Integer userId;
+ String name;
+ String provider;
+ Instant expirationDate;
+}
diff --git a/src/main/java/com/dmdev/dto/CreateUserDto.java b/src/main/java/com/dmdev/dto/CreateUserDto.java
index 6524693..8c1a3a4 100644
--- a/src/main/java/com/dmdev/dto/CreateUserDto.java
+++ b/src/main/java/com/dmdev/dto/CreateUserDto.java
@@ -12,4 +12,4 @@ public class CreateUserDto {
String password;
String role;
String gender;
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/dmdev/dto/UserDto.java b/src/main/java/com/dmdev/dto/UserDto.java
deleted file mode 100644
index 63a98d8..0000000
--- a/src/main/java/com/dmdev/dto/UserDto.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.dmdev.dto;
-
-import com.dmdev.entity.Gender;
-import com.dmdev.entity.Role;
-import lombok.Builder;
-import lombok.Value;
-
-import java.time.LocalDate;
-
-@Value
-@Builder
-public class UserDto {
- Integer id;
- String name;
- LocalDate birthday;
- String email;
- String image;
- Role role;
- Gender gender;
-}
diff --git a/src/main/java/com/dmdev/entity/BaseEntity.java b/src/main/java/com/dmdev/entity/BaseEntity.java
new file mode 100644
index 0000000..c9ce703
--- /dev/null
+++ b/src/main/java/com/dmdev/entity/BaseEntity.java
@@ -0,0 +1,6 @@
+package com.dmdev.entity;
+
+public interface BaseEntity {
+
+ K getId();
+}
diff --git a/src/main/java/com/dmdev/entity/Gender.java b/src/main/java/com/dmdev/entity/Gender.java
index 1a18192..7f49cc3 100644
--- a/src/main/java/com/dmdev/entity/Gender.java
+++ b/src/main/java/com/dmdev/entity/Gender.java
@@ -12,4 +12,4 @@ public static Optional find(String gender) {
.filter(it -> it.name().equals(gender))
.findFirst();
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/dmdev/entity/Provider.java b/src/main/java/com/dmdev/entity/Provider.java
new file mode 100644
index 0000000..fb4c56a
--- /dev/null
+++ b/src/main/java/com/dmdev/entity/Provider.java
@@ -0,0 +1,18 @@
+package com.dmdev.entity;
+
+import java.util.Arrays;
+import java.util.Optional;
+
+public enum Provider {
+ GOOGLE, APPLE;
+
+ public static Provider findByName(String name) {
+ return findByNameOpt(name).orElseThrow();
+ }
+
+ public static Optional findByNameOpt(String name) {
+ return Arrays.stream(values())
+ .filter(provider -> provider.name().equalsIgnoreCase(name))
+ .findFirst();
+ }
+}
diff --git a/src/main/java/com/dmdev/entity/Role.java b/src/main/java/com/dmdev/entity/Role.java
index 6c2d48b..c0ea6a8 100644
--- a/src/main/java/com/dmdev/entity/Role.java
+++ b/src/main/java/com/dmdev/entity/Role.java
@@ -12,4 +12,4 @@ public static Optional find(String role) {
.filter(it -> it.name().equals(role))
.findFirst();
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/dmdev/entity/Status.java b/src/main/java/com/dmdev/entity/Status.java
new file mode 100644
index 0000000..1789ac8
--- /dev/null
+++ b/src/main/java/com/dmdev/entity/Status.java
@@ -0,0 +1,5 @@
+package com.dmdev.entity;
+
+public enum Status {
+ ACTIVE, CANCELED, EXPIRED
+}
diff --git a/src/main/java/com/dmdev/entity/Subscription.java b/src/main/java/com/dmdev/entity/Subscription.java
new file mode 100644
index 0000000..7d77a9a
--- /dev/null
+++ b/src/main/java/com/dmdev/entity/Subscription.java
@@ -0,0 +1,23 @@
+package com.dmdev.entity;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import java.time.Instant;
+
+@Data
+@Accessors(chain = true)
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class Subscription implements BaseEntity {
+ private Integer id;
+ private Integer userId;
+ private String name;
+ private Provider provider;
+ private Instant expirationDate;
+ private Status status;
+}
diff --git a/src/main/java/com/dmdev/entity/User.java b/src/main/java/com/dmdev/entity/User.java
index c21da9a..a7b1f29 100644
--- a/src/main/java/com/dmdev/entity/User.java
+++ b/src/main/java/com/dmdev/entity/User.java
@@ -19,4 +19,4 @@ public class User {
private String password;
private Role role;
private Gender gender;
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/dmdev/exception/SubscriptionException.java b/src/main/java/com/dmdev/exception/SubscriptionException.java
new file mode 100644
index 0000000..fa091ce
--- /dev/null
+++ b/src/main/java/com/dmdev/exception/SubscriptionException.java
@@ -0,0 +1,8 @@
+package com.dmdev.exception;
+
+public class SubscriptionException extends RuntimeException {
+
+ public SubscriptionException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/dmdev/exception/ValidationException.java b/src/main/java/com/dmdev/exception/ValidationException.java
index 07a5f84..acc805e 100644
--- a/src/main/java/com/dmdev/exception/ValidationException.java
+++ b/src/main/java/com/dmdev/exception/ValidationException.java
@@ -2,15 +2,13 @@
import com.dmdev.validator.Error;
import lombok.Getter;
+import lombok.RequiredArgsConstructor;
import java.util.List;
+@RequiredArgsConstructor
public class ValidationException extends RuntimeException {
@Getter
private final List errors;
-
- public ValidationException(List errors) {
- this.errors = errors;
- }
}
diff --git a/src/main/java/com/dmdev/mapper/CreateSubscriptionMapper.java b/src/main/java/com/dmdev/mapper/CreateSubscriptionMapper.java
new file mode 100644
index 0000000..8c8f078
--- /dev/null
+++ b/src/main/java/com/dmdev/mapper/CreateSubscriptionMapper.java
@@ -0,0 +1,30 @@
+package com.dmdev.mapper;
+
+import com.dmdev.dto.CreateSubscriptionDto;
+import com.dmdev.entity.Provider;
+import com.dmdev.entity.Status;
+import com.dmdev.entity.Subscription;
+import lombok.NoArgsConstructor;
+
+import static lombok.AccessLevel.PRIVATE;
+
+@NoArgsConstructor(access = PRIVATE)
+public class CreateSubscriptionMapper implements Mapper {
+
+ private static final CreateSubscriptionMapper INSTANCE = new CreateSubscriptionMapper();
+
+ public static CreateSubscriptionMapper getInstance() {
+ return INSTANCE;
+ }
+
+ @Override
+ public Subscription map(CreateSubscriptionDto object) {
+ return Subscription.builder()
+ .userId(object.getUserId())
+ .name(object.getName())
+ .provider(Provider.findByNameOpt(object.getProvider()).orElse(null))
+ .expirationDate(object.getExpirationDate())
+ .status(Status.ACTIVE)
+ .build();
+ }
+}
diff --git a/src/main/java/com/dmdev/mapper/CreateUserMapper.java b/src/main/java/com/dmdev/mapper/CreateUserMapper.java
index ce27132..c72140a 100644
--- a/src/main/java/com/dmdev/mapper/CreateUserMapper.java
+++ b/src/main/java/com/dmdev/mapper/CreateUserMapper.java
@@ -22,7 +22,7 @@ public static CreateUserMapper getInstance() {
public User map(CreateUserDto object) {
return User.builder()
.name(object.getName())
- .birthday(LocalDateFormatter.format(object.getBirthday()))
+ .birthday(LocalDateFormatter.parse(object.getBirthday()))
.email(object.getEmail())
.password(object.getPassword())
.gender(Gender.find(object.getGender()).orElse(null))
diff --git a/src/main/java/com/dmdev/mapper/UserMapper.java b/src/main/java/com/dmdev/mapper/UserMapper.java
deleted file mode 100644
index 3d2e33c..0000000
--- a/src/main/java/com/dmdev/mapper/UserMapper.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.dmdev.mapper;
-
-import com.dmdev.dto.UserDto;
-import com.dmdev.entity.User;
-import lombok.NoArgsConstructor;
-
-import static lombok.AccessLevel.PRIVATE;
-
-@NoArgsConstructor(access = PRIVATE)
-public class UserMapper implements Mapper {
-
- private static final UserMapper INSTANCE = new UserMapper();
-
- public static UserMapper getInstance() {
- return INSTANCE;
- }
-
- @Override
- public UserDto map(User object) {
- return UserDto.builder()
- .id(object.getId())
- .name(object.getName())
- .birthday(object.getBirthday())
- .email(object.getEmail())
- .role(object.getRole())
- .gender(object.getGender())
- .build();
- }
-}
diff --git a/src/main/java/com/dmdev/service/SubscriptionService.java b/src/main/java/com/dmdev/service/SubscriptionService.java
new file mode 100644
index 0000000..9ecddeb
--- /dev/null
+++ b/src/main/java/com/dmdev/service/SubscriptionService.java
@@ -0,0 +1,63 @@
+package com.dmdev.service;
+
+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 lombok.RequiredArgsConstructor;
+
+import java.time.Clock;
+import java.time.Instant;
+
+@RequiredArgsConstructor
+public class SubscriptionService {
+
+ private final SubscriptionDao subscriptionDao;
+ private final CreateSubscriptionMapper createSubscriptionMapper;
+ private final CreateSubscriptionValidator createSubscriptionValidator;
+ private final Clock clock;
+
+ public Subscription upsert(CreateSubscriptionDto dto) {
+ var validationResult = createSubscriptionValidator.validate(dto);
+ if (validationResult.hasErrors()) {
+ throw new ValidationException(validationResult.getErrors());
+ }
+
+ Subscription subscription = subscriptionDao.findByUserId(dto.getUserId()).stream()
+ .filter(existingSubscription -> existingSubscription.getName().equals(dto.getName()))
+ .filter(existingSubscription -> existingSubscription.getProvider() == Provider.findByName(dto.getProvider()))
+ .findFirst()
+ .map(existingSubscription -> existingSubscription
+ .setExpirationDate(dto.getExpirationDate())
+ .setStatus(Status.ACTIVE))
+ .orElseGet(() -> createSubscriptionMapper.map(dto));
+
+ return subscriptionDao.upsert(subscription);
+ }
+
+ public void cancel(Integer subscriptionId) {
+ var subscription = subscriptionDao.findById(subscriptionId)
+ .orElseThrow(IllegalArgumentException::new);
+ if (subscription.getStatus() != Status.ACTIVE) {
+ throw new SubscriptionException(String.format("Only active subscription %d can be canceled", subscriptionId));
+ }
+ subscription.setStatus(Status.CANCELED);
+ subscriptionDao.update(subscription);
+ }
+
+ public void expire(Integer subscriptionId) {
+ var subscription = subscriptionDao.findById(subscriptionId)
+ .orElseThrow(IllegalArgumentException::new);
+ if (subscription.getStatus() == Status.EXPIRED) {
+ throw new SubscriptionException(String.format("Subscription %d has already expired", subscriptionId));
+ }
+ subscription.setStatus(Status.EXPIRED);
+ subscription.setExpirationDate(Instant.now(clock));
+ subscriptionDao.update(subscription);
+ }
+}
diff --git a/src/main/java/com/dmdev/service/UserService.java b/src/main/java/com/dmdev/service/UserService.java
deleted file mode 100644
index b5abbe3..0000000
--- a/src/main/java/com/dmdev/service/UserService.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.dmdev.service;
-
-import com.dmdev.dao.UserDao;
-import com.dmdev.dto.CreateUserDto;
-import com.dmdev.dto.UserDto;
-import com.dmdev.exception.ValidationException;
-import com.dmdev.mapper.CreateUserMapper;
-import com.dmdev.mapper.UserMapper;
-import com.dmdev.validator.CreateUserValidator;
-import lombok.NoArgsConstructor;
-import lombok.SneakyThrows;
-
-import java.util.Optional;
-
-import static lombok.AccessLevel.PRIVATE;
-
-@NoArgsConstructor(access = PRIVATE)
-public class UserService {
-
- private static final UserService INSTANCE = new UserService();
-
- private final CreateUserValidator createUserValidator = CreateUserValidator.getInstance();
- private final UserDao userDao = UserDao.getInstance();
- private final CreateUserMapper createUserMapper = CreateUserMapper.getInstance();
- private final UserMapper userMapper = UserMapper.getInstance();
-
- public static UserService getInstance() {
- return INSTANCE;
- }
-
- public Optional login(String email, String password) {
- return userDao.findByEmailAndPassword(email, password)
- .map(userMapper::map);
- }
-
- @SneakyThrows
- public UserDto create(CreateUserDto userDto) {
- var validationResult = createUserValidator.validate(userDto);
- if (!validationResult.isValid()) {
- throw new ValidationException(validationResult.getErrors());
- }
- var userEntity = createUserMapper.map(userDto);
- userDao.save(userEntity);
-
- return userMapper.map(userEntity);
- }
-}
diff --git a/src/main/java/com/dmdev/util/LocalDateFormatter.java b/src/main/java/com/dmdev/util/LocalDateFormatter.java
index 33e8d2b..7b326c7 100644
--- a/src/main/java/com/dmdev/util/LocalDateFormatter.java
+++ b/src/main/java/com/dmdev/util/LocalDateFormatter.java
@@ -13,14 +13,14 @@ public class LocalDateFormatter {
private static final String PATTERN = "yyyy-MM-dd";
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(PATTERN);
- public LocalDate format(String date) {
+ public LocalDate parse(String date) {
return LocalDate.parse(date, FORMATTER);
}
public boolean isValid(String date) {
try {
return Optional.ofNullable(date)
- .map(LocalDateFormatter::format)
+ .map(LocalDateFormatter::parse)
.isPresent();
} catch (DateTimeParseException exception) {
return false;
diff --git a/src/main/java/com/dmdev/util/PropertiesUtil.java b/src/main/java/com/dmdev/util/PropertiesUtil.java
index d83e928..a46867f 100644
--- a/src/main/java/com/dmdev/util/PropertiesUtil.java
+++ b/src/main/java/com/dmdev/util/PropertiesUtil.java
@@ -6,7 +6,7 @@
import java.util.Properties;
@UtilityClass
-public final class PropertiesUtil {
+public class PropertiesUtil {
private static final Properties properties = new Properties();
diff --git a/src/main/java/com/dmdev/validator/CreateSubscriptionValidator.java b/src/main/java/com/dmdev/validator/CreateSubscriptionValidator.java
new file mode 100644
index 0000000..09c0b2e
--- /dev/null
+++ b/src/main/java/com/dmdev/validator/CreateSubscriptionValidator.java
@@ -0,0 +1,39 @@
+package com.dmdev.validator;
+
+import com.dmdev.dto.CreateSubscriptionDto;
+import com.dmdev.entity.Provider;
+import lombok.NoArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+
+import java.time.Instant;
+
+import static lombok.AccessLevel.PRIVATE;
+
+@NoArgsConstructor(access = PRIVATE)
+public class CreateSubscriptionValidator implements Validator {
+
+ private static final CreateSubscriptionValidator INSTANCE = new CreateSubscriptionValidator();
+
+ public static CreateSubscriptionValidator getInstance() {
+ return INSTANCE;
+ }
+
+ @Override
+ public ValidationResult validate(CreateSubscriptionDto object) {
+ var validationResult = new ValidationResult();
+ if (object.getUserId() == null) {
+ validationResult.add(Error.of(100, "userId is invalid"));
+ }
+ if (StringUtils.isBlank(object.getName())) {
+ validationResult.add(Error.of(101, "name is invalid"));
+ }
+ if (Provider.findByNameOpt(object.getProvider()).isEmpty()) {
+ validationResult.add(Error.of(102, "provider is invalid"));
+ }
+ if (object.getExpirationDate() == null || object.getExpirationDate().isBefore(Instant.now())) {
+ validationResult.add(Error.of(103, "expirationDate is invalid"));
+ }
+
+ return validationResult;
+ }
+}
diff --git a/src/main/java/com/dmdev/validator/CreateUserValidator.java b/src/main/java/com/dmdev/validator/CreateUserValidator.java
index 1d53779..f5f5e2e 100644
--- a/src/main/java/com/dmdev/validator/CreateUserValidator.java
+++ b/src/main/java/com/dmdev/validator/CreateUserValidator.java
@@ -21,14 +21,14 @@ public static CreateUserValidator getInstance() {
public ValidationResult validate(CreateUserDto object) {
var validationResult = new ValidationResult();
if (!LocalDateFormatter.isValid(object.getBirthday())) {
- validationResult.add(Error.of("invalid.birthday", "Birthday is invalid"));
+ validationResult.add(Error.of(104, "Birthday is invalid"));
}
if (Gender.find(object.getGender()).isEmpty()) {
- validationResult.add(Error.of("invalid.gender", "Gender is invalid"));
+ validationResult.add(Error.of(105, "Gender is invalid"));
}
if (Role.find(object.getRole()).isEmpty()) {
- validationResult.add(Error.of("invalid.role", "Role is invalid"));
+ validationResult.add(Error.of(106, "Role is invalid"));
}
return validationResult;
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/dmdev/validator/Error.java b/src/main/java/com/dmdev/validator/Error.java
index acd146a..78686ac 100644
--- a/src/main/java/com/dmdev/validator/Error.java
+++ b/src/main/java/com/dmdev/validator/Error.java
@@ -4,6 +4,6 @@
@Value(staticConstructor = "of")
public class Error {
- String code;
+ Integer code;
String message;
}
diff --git a/src/main/java/com/dmdev/validator/ValidationResult.java b/src/main/java/com/dmdev/validator/ValidationResult.java
index c6f2106..78ba497 100644
--- a/src/main/java/com/dmdev/validator/ValidationResult.java
+++ b/src/main/java/com/dmdev/validator/ValidationResult.java
@@ -14,6 +14,10 @@ public void add(Error error) {
this.errors.add(error);
}
+ public boolean hasErrors() {
+ return !errors.isEmpty();
+ }
+
public boolean isValid() {
return errors.isEmpty();
}
diff --git a/src/test/java/com/dmdev/integration/IntegrationTestBase.java b/src/test/java/com/dmdev/integration/IntegrationTestBase.java
index 2188cc1..86d1a3b 100644
--- a/src/test/java/com/dmdev/integration/IntegrationTestBase.java
+++ b/src/test/java/com/dmdev/integration/IntegrationTestBase.java
@@ -1,41 +1,40 @@
package com.dmdev.integration;
import com.dmdev.util.ConnectionManager;
-import lombok.SneakyThrows;
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
+import java.sql.SQLException;
+
public abstract class IntegrationTestBase {
- private static final String CLEAN_SQL = "DROP TABLE IF EXISTS users;";
+ private static final String CLEAN_SQL = "DELETE FROM subscription;";
private static final String CREATE_SQL = """
- CREATE TABLE IF NOT EXISTS users
+ CREATE TABLE IF NOT EXISTS subscription
(
id INT AUTO_INCREMENT PRIMARY KEY ,
- name VARCHAR(64),
- birthday DATE NOT NULL ,
- email VARCHAR(64) NOT NULL UNIQUE ,
- password VARCHAR(64) NOT NULL ,
- role VARCHAR(32) NOT NULL ,
- gender VARCHAR(16)
+ user_id INT NOT NULL ,
+ name VARCHAR(64) NOT NULL ,
+ provider VARCHAR(16) NOT NULL ,
+ expiration_date DATETIME NOT NULL ,
+ status VARCHAR(16) NOT NULL ,
+ UNIQUE (user_id, name)
);
""";
- private static final String INSERT_SQL = """
- INSERT INTO users (name, birthday, email, password, role, gender)
- VALUES ('Ivan', '1990-01-10', 'ivan@gmail.com', '111', 'ADMIN', 'MALE'),
- ('Petr', '1995-10-19', 'petr@gmail.com', '123', 'USER', 'MALE'),
- ('Sveta', '2001-12-23', 'sveta@gmail.com', '321', 'USER', 'FEMALE'),
- ('Vlad', '1984-03-14', 'vlad@gmail.com', '456', 'USER', 'MALE'),
- ('Kate', '1989-08-09', 'kate@gmail.com', '777', 'ADMIN', 'FEMALE');
- """;
+
+ @BeforeAll
+ static void prepareDatabase() throws SQLException {
+ try (var connection = ConnectionManager.get();
+ var statement = connection.createStatement()) {
+ statement.execute(CREATE_SQL);
+ }
+ }
@BeforeEach
- @SneakyThrows
- void prepareDatabase() {
+ void cleanData() throws SQLException {
try (var connection = ConnectionManager.get();
var statement = connection.createStatement()) {
statement.execute(CLEAN_SQL);
- statement.execute(CREATE_SQL);
- statement.execute(INSERT_SQL);
}
}
}
diff --git a/src/test/java/com/dmdev/mapper/CreateUserMapperTest.java b/src/test/java/com/dmdev/mapper/CreateUserMapperTest.java
new file mode 100644
index 0000000..da86b92
--- /dev/null
+++ b/src/test/java/com/dmdev/mapper/CreateUserMapperTest.java
@@ -0,0 +1,42 @@
+package com.dmdev.mapper;
+
+import com.dmdev.dto.CreateUserDto;
+import com.dmdev.entity.Gender;
+import com.dmdev.entity.Role;
+import com.dmdev.entity.User;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDate;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class CreateUserMapperTest {
+
+ private static final CreateUserMapper mapper = CreateUserMapper.getInstance();
+
+ @Test
+ void whenDtoIsValid_thenUserIsValid() {
+
+ CreateUserDto validDto = CreateUserDto.builder()
+ .name("Mike Pierson")
+ .birthday("2001-01-01")
+ .email("test@gmail.com")
+ .password("test")
+ .role(Role.USER.name())
+ .gender(Gender.MALE.name())
+ .build();
+
+ User actualUser = mapper.map(validDto);
+
+ User validUser = User.builder()
+ .name("Mike Pierson")
+ .birthday(LocalDate.of(2001, 1, 1))
+ .email("test@gmail.com")
+ .password("test")
+ .role(Role.USER)
+ .gender(Gender.MALE)
+ .build();
+
+ assertThat(actualUser).isEqualTo(validUser);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/dmdev/util/LocalDateFormatterTest.java b/src/test/java/com/dmdev/util/LocalDateFormatterTest.java
new file mode 100644
index 0000000..a6dc612
--- /dev/null
+++ b/src/test/java/com/dmdev/util/LocalDateFormatterTest.java
@@ -0,0 +1,44 @@
+package com.dmdev.util;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.time.LocalDate;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+class LocalDateFormatterTest {
+
+ @Test
+ void whenValidDate_thenParsedSuccessfully() {
+ LocalDate actual = LocalDateFormatter.parse("2009-03-25");
+ LocalDate expected = LocalDate.of(2009, 3, 25);
+ assertThat(expected).isEqualTo(actual);
+ }
+
+ @Test
+ void whenInValidDate_thenExceptionThrown() {
+ assertThatThrownBy(() -> LocalDateFormatter.parse("209-03-25"));
+ assertThatThrownBy(() -> LocalDateFormatter.parse(""));
+ }
+
+ @ParameterizedTest
+ @MethodSource("getDatesTestCases")
+ void checkIfDateStringsValidOrNot(String date, boolean expected) {
+ boolean actual = LocalDateFormatter.isValid(date);
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ static Stream getDatesTestCases() {
+ return Stream.of(
+ arguments("2009-01-01", true),
+ arguments("2009-13-13", false),
+ arguments("", false)
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/dmdev/validator/CreateUserValidatorTest.java b/src/test/java/com/dmdev/validator/CreateUserValidatorTest.java
new file mode 100644
index 0000000..c9c3611
--- /dev/null
+++ b/src/test/java/com/dmdev/validator/CreateUserValidatorTest.java
@@ -0,0 +1,97 @@
+package com.dmdev.validator;
+
+import com.dmdev.dto.CreateUserDto;
+import com.dmdev.entity.Gender;
+import com.dmdev.entity.Role;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class CreateUserValidatorTest {
+
+ private final CreateUserValidator validator = CreateUserValidator.getInstance();
+
+ @Test
+ void whenDtoIsValid_thenValidationSuccessful() {
+ CreateUserDto dto = CreateUserDto.builder()
+ .name("Mike Pierson")
+ .birthday("2001-01-01")
+ .email("test@gmail.com")
+ .password("test")
+ .role(Role.USER.name())
+ .gender(Gender.MALE.name())
+ .build();
+
+ ValidationResult actual = validator.validate(dto);
+
+ assertThat(actual.isValid()).isTrue();
+ }
+
+ @Test
+ void whenBirthdayIsInvalid_thenValidationFails() {
+ CreateUserDto dto = CreateUserDto.builder()
+ .name("Mike Pierson")
+ .birthday("201-01-01")
+ .email("test@gmail.com")
+ .password("test")
+ .role(Role.USER.name())
+ .gender(Gender.MALE.name())
+ .build();
+
+ ValidationResult actual = validator.validate(dto);
+
+ assertThat(actual.isValid()).isFalse();
+ assertThat(actual.getErrors().get(0).getMessage()).isEqualTo("Birthday is invalid");
+ }
+
+ @Test
+ void whenRoleIsInvalid_thenValidationFails() {
+ CreateUserDto dto = CreateUserDto.builder()
+ .name("Mike Pierson")
+ .birthday("2001-01-01")
+ .email("test@gmail.com")
+ .password("test")
+ .role("invalid role")
+ .gender(Gender.MALE.name())
+ .build();
+
+ ValidationResult actual = validator.validate(dto);
+
+ assertThat(actual.isValid()).isFalse();
+ assertThat(actual.getErrors().get(0).getMessage()).isEqualTo("Role is invalid");
+ }
+
+ @Test
+ void whenGenderIsInvalid_thenValidationFails() {
+ CreateUserDto dto = CreateUserDto.builder()
+ .name("Mike Pierson")
+ .birthday("2001-01-01")
+ .email("test@gmail.com")
+ .password("test")
+ .role(Role.USER.name())
+ .gender("invalid gender")
+ .build();
+
+ ValidationResult actual = validator.validate(dto);
+
+ assertThat(actual.isValid()).isFalse();
+ assertThat(actual.getErrors().get(0).getMessage()).isEqualTo("Gender is invalid");
+ }
+
+ @Test
+ void whenCheckedFieldsAreInvalid_thenValidationFails() {
+ CreateUserDto dto = CreateUserDto.builder()
+ .name("Mike Pierson")
+ .birthday("invalid birthday")
+ .email("test@gmail.com")
+ .password("test")
+ .role("invalid role")
+ .gender("invalid gender")
+ .build();
+
+ ValidationResult actual = validator.validate(dto);
+
+ assertThat(actual.isValid()).isFalse();
+ assertThat(actual.getErrors().stream().map(e -> e.getCode()).toList()).contains(104, 105, 106);
+ }
+}
\ No newline at end of file