diff --git a/pom.xml b/pom.xml
index 463c291..071ff17 100644
--- a/pom.xml
+++ b/pom.xml
@@ -76,6 +76,21 @@
lombok
true
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
+
+ com.h2database
+ h2
+ test
+
diff --git a/src/main/java/com/example/bankcards/service/AuthController.java b/src/main/java/com/example/bankcards/controller/AuthController.java
similarity index 100%
rename from src/main/java/com/example/bankcards/service/AuthController.java
rename to src/main/java/com/example/bankcards/controller/AuthController.java
diff --git a/src/test/java/com/example/bankcards/service/CardServiceTest.java b/src/test/java/com/example/bankcards/service/CardServiceTest.java
new file mode 100644
index 0000000..8eac649
--- /dev/null
+++ b/src/test/java/com/example/bankcards/service/CardServiceTest.java
@@ -0,0 +1,147 @@
+package com.example.bankcards.service;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+import com.example.bankcards.dto.CreateCardRequest;
+import com.example.bankcards.dto.TransferRequest;
+import com.example.bankcards.entity.Card;
+import com.example.bankcards.entity.CardStatus;
+import com.example.bankcards.entity.Role;
+import com.example.bankcards.entity.User;
+import com.example.bankcards.exception.BadRequestException;
+import com.example.bankcards.repository.CardRepository;
+import com.example.bankcards.repository.UserRepository;
+import com.example.bankcards.util.CardEncryptionUtil;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+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;
+
+@ExtendWith(MockitoExtension.class)
+class CardServiceTest {
+
+ @Mock
+ private CardRepository cardRepository;
+
+ @Mock
+ private UserRepository userRepository;
+
+ @Mock
+ private CardEncryptionUtil encryptionUtil;
+
+ @InjectMocks
+ private CardService cardService;
+
+ private User user;
+ private Card activeCard;
+ private Card blockedCard;
+
+ @BeforeEach
+ void setUp() {
+ user = User.builder()
+ .id(1L)
+ .username("testuser")
+ .role(Role.ROLE_USER)
+ .build();
+ activeCard = Card.builder()
+ .id(1L)
+ .owner(user)
+ .status(CardStatus.ACTIVE)
+ .balance(new BigDecimal("1000"))
+ .cardNumberEncrypted("enc1")
+ .expiryDate(LocalDate.now().plusYears(2))
+ .build();
+ blockedCard = Card.builder()
+ .id(2L)
+ .owner(user)
+ .status(CardStatus.BLOCKED)
+ .balance(new BigDecimal("500"))
+ .cardNumberEncrypted("enc2")
+ .expiryDate(LocalDate.now().plusYears(2))
+ .build();
+ }
+
+ @Test
+ void createCard_success() {
+ CreateCardRequest req = new CreateCardRequest();
+ req.setCardNumber("1234567890123456");
+ req.setOwnerId(1L);
+ req.setExpiryDate(LocalDate.now().plusYears(2));
+ req.setInitialBalance(BigDecimal.valueOf(1000));
+
+ when(userRepository.findById(1L)).thenReturn(Optional.of(user));
+ when(encryptionUtil.encrypt("1234567890123456")).thenReturn(
+ "encrypted"
+ );
+ when(cardRepository.save(any())).thenReturn(activeCard);
+ when(encryptionUtil.decrypt("enc1")).thenReturn("1234567890123456");
+ when(encryptionUtil.mask("1234567890123456")).thenReturn(
+ "**** **** **** 3456"
+ );
+
+ var response = cardService.createCard(req);
+ assertThat(response).isNotNull();
+ verify(cardRepository, times(1)).save(any());
+ }
+
+ @Test
+ void transfer_insufficientBalance_throwsBadRequest() {
+ TransferRequest req = new TransferRequest();
+ req.setFromCardId(1L);
+ req.setToCardId(2L);
+ req.setAmount(new BigDecimal("5000"));
+
+ Card toCard = Card.builder()
+ .id(2L)
+ .owner(user)
+ .status(CardStatus.ACTIVE)
+ .balance(BigDecimal.ZERO)
+ .cardNumberEncrypted("enc2")
+ .expiryDate(LocalDate.now().plusYears(2))
+ .build();
+
+ when(cardRepository.findById(1L)).thenReturn(Optional.of(activeCard));
+ when(cardRepository.findById(2L)).thenReturn(Optional.of(toCard));
+
+ assertThatThrownBy(() -> cardService.transfer(req, "testuser"))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessageContaining("Insufficient balance");
+ }
+
+ @Test
+ void transfer_fromBlockedCard_throwsBadRequest() {
+ TransferRequest req = new TransferRequest();
+ req.setFromCardId(2L);
+ req.setToCardId(1L);
+ req.setAmount(new BigDecimal("100"));
+
+ when(cardRepository.findById(2L)).thenReturn(Optional.of(blockedCard));
+ when(cardRepository.findById(1L)).thenReturn(Optional.of(activeCard));
+
+ assertThatThrownBy(() -> cardService.transfer(req, "testuser"))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessageContaining("not active");
+ }
+
+ @Test
+ void blockCard_success() {
+ when(cardRepository.findById(1L)).thenReturn(Optional.of(activeCard));
+ when(cardRepository.save(any())).thenReturn(activeCard);
+ when(encryptionUtil.decrypt("enc1")).thenReturn("1234567890123456");
+ when(encryptionUtil.mask("1234567890123456")).thenReturn(
+ "**** **** **** 3456"
+ );
+
+ var response = cardService.blockCard(1L);
+ assertThat(response).isNotNull();
+ verify(cardRepository).save(any());
+ }
+}
diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml
new file mode 100644
index 0000000..933d53a
--- /dev/null
+++ b/src/test/resources/application-test.yml
@@ -0,0 +1,19 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1
+ username: sa
+ password:
+ driver-class-name: org.h2.Driver
+ jpa:
+ hibernate:
+ ddl-auto: create-drop
+ database-platform: org.hibernate.dialect.H2Dialect
+ liquibase:
+ enabled: false
+
+app:
+ jwt:
+ secret: 404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970
+ expiration: 86400000
+ card:
+ encryption-key: 1234567890123456