diff --git a/pom.xml b/pom.xml index 3d8377c..47c1e0b 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,17 @@ postgresql test + + org.springframework.security + spring-security-test + test + + + org.projectlombok + lombok + true + + @@ -72,8 +83,16 @@ org.springframework.boot spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + - + \ No newline at end of file diff --git a/src/main/java/com/skypro/simplebanking/controller/AccountController.java b/src/main/java/com/skypro/simplebanking/controller/AccountController.java index efa764b..243bf2a 100644 --- a/src/main/java/com/skypro/simplebanking/controller/AccountController.java +++ b/src/main/java/com/skypro/simplebanking/controller/AccountController.java @@ -32,8 +32,8 @@ public AccountDTO depositToAccount(Authentication authentication, @PostMapping("/withdraw/{id}") public AccountDTO withdrawFromAccount(Authentication authentication, - @PathVariable("id") Long accountId, - @RequestBody BalanceChangeRequest balanceChangeRequest){ + @PathVariable("id") Long accountId, + @RequestBody BalanceChangeRequest balanceChangeRequest){ BankingUserDetails bankingUserDetails = (BankingUserDetails) authentication.getPrincipal(); return accountService.withdrawFromAccount(bankingUserDetails.getId(),accountId, balanceChangeRequest.getAmount()); } diff --git a/src/main/java/com/skypro/simplebanking/controller/TransferController.java b/src/main/java/com/skypro/simplebanking/controller/TransferController.java index 7a8ef7c..516ef54 100644 --- a/src/main/java/com/skypro/simplebanking/controller/TransferController.java +++ b/src/main/java/com/skypro/simplebanking/controller/TransferController.java @@ -20,8 +20,8 @@ public TransferController(TransferService transferService) { @PostMapping public void transfer( - Authentication authentication, @RequestBody TransferRequest transferRequest) { - BankingUserDetails bankingUserDetails = (BankingUserDetails) authentication.getPrincipal(); - transferService.transfer(bankingUserDetails.getId(), transferRequest); + Authentication authentication, @RequestBody TransferRequest transferRequest) { + BankingUserDetails userDetails = (BankingUserDetails) authentication.getPrincipal(); + transferService.transfer(userDetails.getId(), transferRequest); } } diff --git a/src/main/java/com/skypro/simplebanking/controller/UserController.java b/src/main/java/com/skypro/simplebanking/controller/UserController.java index 3f38118..ca60bd5 100644 --- a/src/main/java/com/skypro/simplebanking/controller/UserController.java +++ b/src/main/java/com/skypro/simplebanking/controller/UserController.java @@ -7,6 +7,7 @@ import javax.validation.Valid; import com.skypro.simplebanking.service.UserService; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; @@ -22,8 +23,9 @@ public UserController(UserService userService) { } @PostMapping + @PreAuthorize("hasRole('ADMIN')") public UserDTO createUser(@RequestBody @Valid CreateUserRequest userRequest) { - return userService.createUser(userRequest.getUsername(), userRequest.getPassword()); + return userService.createUser(userRequest.getUsername(), userRequest.getPassword(), userRequest.getRole()); } @GetMapping("/list") public List getAllUsers(){ diff --git a/src/main/java/com/skypro/simplebanking/dto/BalanceChangeRequest.java b/src/main/java/com/skypro/simplebanking/dto/BalanceChangeRequest.java index 1c6e605..07f26a8 100644 --- a/src/main/java/com/skypro/simplebanking/dto/BalanceChangeRequest.java +++ b/src/main/java/com/skypro/simplebanking/dto/BalanceChangeRequest.java @@ -1,10 +1,19 @@ package com.skypro.simplebanking.dto; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.NoArgsConstructor; import org.hibernate.validator.constraints.Range; +@NoArgsConstructor public class BalanceChangeRequest { + @JsonProperty("amount") private long amount; + + public BalanceChangeRequest(long amount) { + this.amount = amount; + } + public long getAmount() { return amount; } @@ -12,4 +21,4 @@ public long getAmount() { public void setAmount(long amount) { this.amount = amount; } -} +} \ No newline at end of file diff --git a/src/main/java/com/skypro/simplebanking/dto/BankingUserDetails.java b/src/main/java/com/skypro/simplebanking/dto/BankingUserDetails.java index 480896e..5989401 100644 --- a/src/main/java/com/skypro/simplebanking/dto/BankingUserDetails.java +++ b/src/main/java/com/skypro/simplebanking/dto/BankingUserDetails.java @@ -23,9 +23,9 @@ public BankingUserDetails(long id, String username, String password, boolean isA @Override public Collection getAuthorities() { SimpleGrantedAuthority authority = - isAdmin - ? new SimpleGrantedAuthority("ROLE_ADMIN") - : new SimpleGrantedAuthority("ROLE_USER"); + isAdmin + ? new SimpleGrantedAuthority("ROLE_ADMIN") + : new SimpleGrantedAuthority("ROLE_USER"); return Collections.singleton(authority); } diff --git a/src/main/java/com/skypro/simplebanking/dto/CreateUserRequest.java b/src/main/java/com/skypro/simplebanking/dto/CreateUserRequest.java index 05d6dba..e27dc51 100644 --- a/src/main/java/com/skypro/simplebanking/dto/CreateUserRequest.java +++ b/src/main/java/com/skypro/simplebanking/dto/CreateUserRequest.java @@ -6,6 +6,7 @@ public class CreateUserRequest { private String username; private String password; + private String role; public String getUsername() { return username; @@ -22,4 +23,12 @@ public String getPassword() { public void setPassword(String password) { this.password = password; } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } } diff --git a/src/main/java/com/skypro/simplebanking/dto/ListUserDTO.java b/src/main/java/com/skypro/simplebanking/dto/ListUserDTO.java index 3404ae1..c913974 100644 --- a/src/main/java/com/skypro/simplebanking/dto/ListUserDTO.java +++ b/src/main/java/com/skypro/simplebanking/dto/ListUserDTO.java @@ -30,8 +30,8 @@ public List getAccounts() { public static ListUserDTO from(User user) { return new ListUserDTO( - user.getId(), - user.getUsername(), - user.getAccounts().stream().map(ListAccountDTO::from).collect(Collectors.toList())); + user.getId(), + user.getUsername(), + user.getAccounts().stream().map(ListAccountDTO::from).collect(Collectors.toList())); } } diff --git a/src/main/java/com/skypro/simplebanking/dto/TransferRequest.java b/src/main/java/com/skypro/simplebanking/dto/TransferRequest.java index 5c4a2f8..c4c702d 100644 --- a/src/main/java/com/skypro/simplebanking/dto/TransferRequest.java +++ b/src/main/java/com/skypro/simplebanking/dto/TransferRequest.java @@ -6,6 +6,12 @@ public class TransferRequest { private long toAccountId; private long amount; + public TransferRequest(long fromAccountId, long toAccountId, long amount) { + this.fromAccountId = fromAccountId; + this.toAccountId = toAccountId; + this.amount = amount; + } + public long getToUserId() { return toUserId; } @@ -37,4 +43,14 @@ public void setFromAccountId(long fromAccountId) { public void setToAccountId(long toAccountId) { this.toAccountId = toAccountId; } + + @Override + public String toString() { + return "TransferRequest{" + + "fromAccountId=" + fromAccountId + + ", toUserId=" + toUserId + + ", toAccountId=" + toAccountId + + ", amount=" + amount + + '}'; + } } diff --git a/src/main/java/com/skypro/simplebanking/dto/UserDTO.java b/src/main/java/com/skypro/simplebanking/dto/UserDTO.java index 76544aa..b67fc81 100644 --- a/src/main/java/com/skypro/simplebanking/dto/UserDTO.java +++ b/src/main/java/com/skypro/simplebanking/dto/UserDTO.java @@ -29,8 +29,8 @@ public List getAccounts() { public static UserDTO from(User user) { return new UserDTO( - user.getId(), - user.getUsername(), - user.getAccounts().stream().map(AccountDTO::from).collect(Collectors.toList())); + user.getId(), + user.getUsername(), + user.getAccounts().stream().map(AccountDTO::from).collect(Collectors.toList())); } -} +} \ No newline at end of file diff --git a/src/main/java/com/skypro/simplebanking/entity/Account.java b/src/main/java/com/skypro/simplebanking/entity/Account.java index 5c9e036..842bccd 100644 --- a/src/main/java/com/skypro/simplebanking/entity/Account.java +++ b/src/main/java/com/skypro/simplebanking/entity/Account.java @@ -48,4 +48,4 @@ public User getUser() { public void setUser(User user) { this.user = user; } -} +} \ No newline at end of file diff --git a/src/main/java/com/skypro/simplebanking/entity/Role.java b/src/main/java/com/skypro/simplebanking/entity/Role.java new file mode 100644 index 0000000..819601e --- /dev/null +++ b/src/main/java/com/skypro/simplebanking/entity/Role.java @@ -0,0 +1,7 @@ +package com.skypro.simplebanking.entity; + + +public enum Role { + ROLE_USER, + ROLE_ADMIN +} diff --git a/src/main/java/com/skypro/simplebanking/entity/User.java b/src/main/java/com/skypro/simplebanking/entity/User.java index d9290c6..90b3d94 100644 --- a/src/main/java/com/skypro/simplebanking/entity/User.java +++ b/src/main/java/com/skypro/simplebanking/entity/User.java @@ -12,10 +12,15 @@ public class User { @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user-generator") @SequenceGenerator(name = "user-generator", sequenceName = "user_sequence") private Long id; + @Column(name = "username", unique = true) private String username; + @Column(name = "password") private String password; @OneToMany(cascade = CascadeType.ALL, mappedBy = "user") private Collection accounts; + @Column(name = "role") + @Enumerated(EnumType.STRING) + private Role role; public Long getId() { return id; @@ -48,4 +53,12 @@ public Collection getAccounts() { public void setAccounts(Collection accounts) { this.accounts = accounts; } -} + + public Role getRole() { + return role; + } + + public void setRole(Role role) { + this.role = role; + } +} \ No newline at end of file diff --git a/src/main/java/com/skypro/simplebanking/exception/UserNotFoundException.java b/src/main/java/com/skypro/simplebanking/exception/UserNotFoundException.java new file mode 100644 index 0000000..adf6d75 --- /dev/null +++ b/src/main/java/com/skypro/simplebanking/exception/UserNotFoundException.java @@ -0,0 +1,15 @@ +package com.skypro.simplebanking.exception; + +public class UserNotFoundException extends RuntimeException { + public UserNotFoundException() { + super("User not found"); + } + + public UserNotFoundException(String message) { + super(message); + } + + public UserNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/skypro/simplebanking/service/AccountService.java b/src/main/java/com/skypro/simplebanking/service/AccountService.java index b4d8ce4..e2263bf 100644 --- a/src/main/java/com/skypro/simplebanking/service/AccountService.java +++ b/src/main/java/com/skypro/simplebanking/service/AccountService.java @@ -4,12 +4,11 @@ import com.skypro.simplebanking.entity.Account; import com.skypro.simplebanking.entity.AccountCurrency; import com.skypro.simplebanking.entity.User; -import com.skypro.simplebanking.exception.AccountNotFoundException; -import com.skypro.simplebanking.exception.InsufficientFundsException; -import com.skypro.simplebanking.exception.InvalidAmountException; -import com.skypro.simplebanking.exception.WrongCurrencyException; +import com.skypro.simplebanking.exception.*; import com.skypro.simplebanking.repository.AccountRepository; import java.util.ArrayList; + +import com.skypro.simplebanking.repository.UserRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -17,9 +16,11 @@ @Service public class AccountService { private final AccountRepository accountRepository; + private final UserRepository userRepository; - public AccountService(AccountRepository accountRepository) { + public AccountService(AccountRepository accountRepository, UserRepository userRepository) { this.accountRepository = accountRepository; + this.userRepository = userRepository; } @Transactional(propagation = Propagation.MANDATORY) @@ -30,7 +31,6 @@ public void createDefaultAccounts(User user) { account.setUser(user); account.setAccountCurrency(currency); account.setAmount(1L); - user.getAccounts().add(account); accountRepository.save(account); } } @@ -38,18 +38,16 @@ public void createDefaultAccounts(User user) { @Transactional(readOnly = true) public AccountDTO getAccount(long userId, Long accountId) { return accountRepository - .getAccountByUser_IdAndId(userId, accountId) - .map(AccountDTO::from) - .orElseThrow(AccountNotFoundException::new); + .getAccountByUser_IdAndId(userId, accountId) + .map(AccountDTO::from) + .orElseThrow(AccountNotFoundException::new); } @Transactional public void validateCurrency(long sourceAccount, long destinationAccount) { - Account acc1 = - accountRepository.findById(sourceAccount).orElseThrow(AccountNotFoundException::new); - Account acc2 = - accountRepository.findById(destinationAccount).orElseThrow(AccountNotFoundException::new); - if (!acc1.getAccountCurrency().equals(acc2.getAccountCurrency())){ + Account acc1 = accountRepository.findById(sourceAccount).orElseThrow(AccountNotFoundException::new); + Account acc2 = accountRepository.findById(destinationAccount).orElseThrow(AccountNotFoundException::new); + if (!acc1.getAccountCurrency().equals(acc2.getAccountCurrency())) { throw new WrongCurrencyException(); } } @@ -59,8 +57,7 @@ public AccountDTO depositToAccount(long userId, Long accountId, long amount) { if (amount < 0) { throw new InvalidAmountException(); } - Account account = - accountRepository + Account account = accountRepository .getAccountByUser_IdAndId(userId, accountId) .orElseThrow(AccountNotFoundException::new); account.setAmount(account.getAmount() + amount); @@ -72,15 +69,67 @@ public AccountDTO withdrawFromAccount(long id, Long accountId, long amount) { if (amount < 0) { throw new InvalidAmountException(); } - Account account = - accountRepository + Account account = accountRepository .getAccountByUser_IdAndId(id, accountId) .orElseThrow(AccountNotFoundException::new); if (account.getAmount() < amount) { - throw new InsufficientFundsException( - "Cannot withdraw " + amount + " " + account.getAccountCurrency().name()); + throw new InsufficientFundsException("Cannot withdraw " + amount + " " + account.getAccountCurrency().name()); } account.setAmount(account.getAmount() - amount); return AccountDTO.from(account); } + + @Transactional + public void transferBetweenAccounts(long userId, Long sourceAccountId, Long destinationAccountId, long amount) { + if (amount < 0) { + throw new InvalidAmountException(); + } + Account sourceAccount = getAccountByUserIdAndAccountId(userId, sourceAccountId); + Account destinationAccount = getAccountByAccountId(destinationAccountId); + + if (!sourceAccount.getAccountCurrency().equals(destinationAccount.getAccountCurrency())) { + throw new WrongCurrencyException(); + } + + if (sourceAccount.getAmount() < amount) { + throw new InsufficientFundsException("Cannot transfer " + amount + " " + sourceAccount.getAccountCurrency().name() + " from account " + sourceAccountId); + } + + sourceAccount.setAmount(sourceAccount.getAmount() - amount); + destinationAccount.setAmount(destinationAccount.getAmount() + amount); + } + + @Transactional + public AccountDTO createAccount(Long userId, long initialBalance) { + Account account = new Account(); + account.setUser(userRepository.findById(userId).orElseThrow(UserNotFoundException::new)); + account.setAmount(initialBalance); + account.setAccountCurrency(AccountCurrency.USD); + Account saved = accountRepository.save(account); + return AccountDTO.from(saved); + } + + @Transactional + public void changeAccountCurrency(Long accountId, AccountCurrency newCurrency) { + Account account = accountRepository.findById(accountId).orElseThrow(AccountNotFoundException::new); + account.setAccountCurrency(newCurrency); + } + private Account getAccountByUserIdAndAccountId(long userId, Long accountId) { + return accountRepository.getAccountByUser_IdAndId(userId, accountId).orElseThrow(AccountNotFoundException::new); + } + + private Account getAccountByAccountId(Long accountId) { + return accountRepository.findById(accountId).orElseThrow(AccountNotFoundException::new); + } + @Transactional + public AccountDTO createTestAccount(Long userId, long initialBalance, Long accountId) { + Account account = new Account(); + account.setId(accountId); + account.setUser(userRepository.findById(userId).orElseThrow(UserNotFoundException::new)); + account.setAmount(initialBalance); + account.setAccountCurrency(AccountCurrency.USD); + Account saved = accountRepository.save(account); + return AccountDTO.from(saved); + } + } diff --git a/src/main/java/com/skypro/simplebanking/service/TransferService.java b/src/main/java/com/skypro/simplebanking/service/TransferService.java index 80bc0eb..f743d89 100644 --- a/src/main/java/com/skypro/simplebanking/service/TransferService.java +++ b/src/main/java/com/skypro/simplebanking/service/TransferService.java @@ -1,10 +1,12 @@ package com.skypro.simplebanking.service; + import com.skypro.simplebanking.dto.TransferRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RequestBody; + @Service public class TransferService { private final AccountService accountService; @@ -14,14 +16,14 @@ public TransferService(AccountService accountService) { } @Transactional - public void transfer(long id, @RequestBody TransferRequest transferRequest) { + public void transfer(Long id, TransferRequest transferRequest) { accountService.validateCurrency( - transferRequest.getFromAccountId(), transferRequest.getToAccountId()); + transferRequest.getFromAccountId(), transferRequest.getToAccountId()); accountService.withdrawFromAccount( - id, transferRequest.getFromAccountId(), transferRequest.getAmount()); + id, transferRequest.getFromAccountId(), transferRequest.getAmount()); accountService.depositToAccount( - transferRequest.getToUserId(), - transferRequest.getToAccountId(), - transferRequest.getAmount()); + transferRequest.getToUserId(), + transferRequest.getToAccountId(), + transferRequest.getAmount()); } } diff --git a/src/main/java/com/skypro/simplebanking/service/UserService.java b/src/main/java/com/skypro/simplebanking/service/UserService.java index 9f06d7b..72fe8ac 100644 --- a/src/main/java/com/skypro/simplebanking/service/UserService.java +++ b/src/main/java/com/skypro/simplebanking/service/UserService.java @@ -1,14 +1,16 @@ package com.skypro.simplebanking.service; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + import com.skypro.simplebanking.dto.BankingUserDetails; import com.skypro.simplebanking.dto.ListUserDTO; import com.skypro.simplebanking.dto.UserDTO; +import com.skypro.simplebanking.entity.Role; import com.skypro.simplebanking.entity.User; import com.skypro.simplebanking.exception.UserAlreadyExistsException; import com.skypro.simplebanking.repository.UserRepository; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; @@ -16,6 +18,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; + @Service public class UserService implements UserDetailsService { private final UserRepository userRepository; @@ -23,9 +26,9 @@ public class UserService implements UserDetailsService { private final PasswordEncoder passwordEncoder; public UserService( - UserRepository userRepository, - AccountService accountService, - PasswordEncoder passwordEncoder) { + UserRepository userRepository, + AccountService accountService, + PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.accountService = accountService; this.passwordEncoder = passwordEncoder; @@ -35,13 +38,13 @@ public UserService( @Transactional(readOnly = true) public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return userRepository - .findByUsername(username) - .map(BankingUserDetails::from) - .orElseThrow(() -> new UsernameNotFoundException("User not found")); + .findByUsername(username) + .map(BankingUserDetails::from) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); } @Transactional - public UserDTO createUser(String username, String password) { + public UserDTO createUser(String username, String password, String role) { Optional existingUser = userRepository.findByUsername(username); if (existingUser.isPresent()) { throw new UserAlreadyExistsException(); @@ -49,9 +52,10 @@ public UserDTO createUser(String username, String password) { User user = new User(); user.setUsername(username); user.setPassword(passwordEncoder.encode(password)); - userRepository.save(user); - accountService.createDefaultAccounts(user); - return UserDTO.from(user); + user.setRole(Role.valueOf(role)); + User saved = userRepository.save(user); + accountService.createDefaultAccounts(saved); + return UserDTO.from(saved); } @Transactional(readOnly = true) public UserDTO getUser(long id) { @@ -61,4 +65,23 @@ public UserDTO getUser(long id) { public List listUsers() { return userRepository.findAll().stream().map(ListUserDTO::from).collect(Collectors.toList()); } -} + @Transactional(readOnly = true) + public long getUserIdByUsername(String username) { + return userRepository + .findByUsername(username) + .map(User::getId) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + } + public void deleteAllUsers() { + userRepository.deleteAll(); + } + + public User createTestUser(String username, String password, Long id) { + User user = new User(); + user.setId(id); + user.setUsername(username); + user.setPassword(passwordEncoder.encode(password)); + userRepository.save(user); + return user; + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 682ccce..ebf761f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,6 +1,8 @@ -spring.datasource.url=jdbc:postgresql://localhost:5432/banking -spring.datasource.username=banking -spring.datasource.password=super-safe-pass +spring.datasource.url=jdbc:postgresql://localhost:5432/postgres +spring.datasource.username=postgres +spring.datasource.password=slavakakraft228 spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.open-in-view=false +spring.liquibase.enabled=false app.security.admin-token=SUPER_SECRET_KEY_FROM_ADMIN +app.env=dev diff --git a/src/test/java/com/skypro/simplebanking/ControllerIntegrationTest/UserControllerIntegrationTest.java b/src/test/java/com/skypro/simplebanking/ControllerIntegrationTest/UserControllerIntegrationTest.java new file mode 100644 index 0000000..2c61b3b --- /dev/null +++ b/src/test/java/com/skypro/simplebanking/ControllerIntegrationTest/UserControllerIntegrationTest.java @@ -0,0 +1,232 @@ +package com.skypro.simplebanking.ControllerIntegrationTest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.skypro.simplebanking.dto.*; +import com.skypro.simplebanking.entity.AccountCurrency; +import com.skypro.simplebanking.service.AccountService; +import com.skypro.simplebanking.service.UserService; +import net.minidev.json.JSONObject; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +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; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.context.annotation.Bean; +import org.springframework.http.MediaType; +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.security.core.userdetails.UserDetails; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.web.servlet.MockMvc; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +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; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +public class UserControllerIntegrationTest { + + @LocalServerPort + private int port; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private UserService userService; + + @Autowired + private AccountService accountService; + + private UserDTO userAdmin; + private UserDTO userUser; + + @BeforeEach + public void setupTestUsers() { + userAdmin = userService.createUser("adminUser", "adminPassword", "ROLE_ADMIN"); + userUser = userService.createUser("regularUser", "regularPassword", "ROLE_USER"); + } + @AfterEach + public void cleanup() { + userService.deleteAllUsers(); + } + + @Test + @WithMockUser(username = "adminUser", password = "adminPassword", roles = "ADMIN") + public void testCreateUserWithAdminRole_Success() throws Exception { + CreateUserRequest userRequest = new CreateUserRequest(); + userRequest.setUsername("newUser"); + userRequest.setPassword("newPassword"); + userRequest.setRole("ROLE_USER"); + + mockMvc.perform(post("/user") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(userRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.username").value("newUser")); + } + + + @Test + @WithMockUser(username = "regularUser", password = "regularPassword", roles = "USER") + public void testGetAllUsersByRegularUser_Success() throws Exception { + mockMvc.perform(get("/user/list")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()").value(2)); + } + + + @Test + public void testDepositToOwnAccount_Success() throws Exception { + UserDetails userDetails = userService.loadUserByUsername(userUser.getUsername()); + long userId = userService.getUserIdByUsername(userUser.getUsername()); + + Long sourceAccountId = createAccountForUserWithCurrency(userId, AccountCurrency.USD); + + BankingUserDetails bankingDetails = new BankingUserDetails(userUser.getId(), userUser.getUsername(), userDetails.getPassword(), false); + + Authentication authentication = new UsernamePasswordAuthenticationToken(bankingDetails, null, bankingDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + + long depositAmount = 500L; + long finalDepositAmount = 1500L; + + mockMvc.perform(post("/account/deposit/{id}", sourceAccountId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new BalanceChangeRequest(depositAmount)))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.amount").value(finalDepositAmount)); + } + + @Test + public void testWithdrawFromOwnAccount_Success() throws Exception { + UserDetails userDetails = userService.loadUserByUsername(userUser.getUsername()); + long userId = userService.getUserIdByUsername(userUser.getUsername()); + + Long sourceAccountId = createAccountForUserWithCurrency(userId, AccountCurrency.USD); + long withdrawAmount = 200L; + BankingUserDetails bankingDetails = new BankingUserDetails(userUser.getId(), userUser.getUsername(), userDetails.getPassword(), false); + + Authentication authentication = new UsernamePasswordAuthenticationToken(bankingDetails, null, bankingDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + + + + mockMvc.perform(post("/account/withdraw/{id}", sourceAccountId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new BalanceChangeRequest(withdrawAmount)))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.amount").value(800L)); + } + + @Test + public void testTransferBetweenOwnAccounts_Success() throws Exception { + + UserDetails userDetails = userService.loadUserByUsername(userUser.getUsername()); + long userId = userService.getUserIdByUsername(userUser.getUsername()); + + Long fromAccountID = createAccountForUserWithCurrency(userId, AccountCurrency.USD); + Long toAccountID = createAccountForUserWithCurrency(userId, AccountCurrency.USD); + long amount = 200L; + + BankingUserDetails bankingDetails = new BankingUserDetails(userUser.getId(), userUser.getUsername(), userDetails.getPassword(), false); + + Authentication authentication = new UsernamePasswordAuthenticationToken(bankingDetails, null, bankingDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + + JSONObject requestBody = new JSONObject(); + requestBody.put("fromAccountId", fromAccountID); + requestBody.put("toAccountId", toAccountID); + requestBody.put("toUserId", userId); + requestBody.put("amount", amount); + + mockMvc.perform(post("/transfer") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody.toString())) + .andExpect(status().isOk()); + + AccountDTO sourceAccount = accountService.getAccount(userId, fromAccountID); + AccountDTO destinationAccount = accountService.getAccount(userId, toAccountID); + + assertThat(sourceAccount.getAmount()).isEqualByComparingTo(800L); + assertThat(destinationAccount.getAmount()).isEqualByComparingTo(1200L); + } + + @Test + public void testTransferBetweenAccounts_WithDifferentCurrencies_Failure() throws Exception { + UserDetails userDetails = userService.loadUserByUsername(userUser.getUsername()); + long userId = userService.getUserIdByUsername(userUser.getUsername()); + + Long sourceAccountId = createAccountForUserWithCurrency(userId, AccountCurrency.USD); + Long destinationAccountId = createAccountForUserWithCurrency(userId, AccountCurrency.EUR); + long transferAmount = 200L; + + BankingUserDetails bankingDetails = new BankingUserDetails(userUser.getId(), userUser.getUsername(), userDetails.getPassword(), false); + + Authentication authentication = new UsernamePasswordAuthenticationToken(bankingDetails, null, bankingDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + + mockMvc.perform(post("/transfer") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new TransferRequest(sourceAccountId, destinationAccountId, transferAmount)))) + .andExpect(status().isBadRequest()); + } + + @Test + public void testWithdrawFromOwnAccount_InsufficientFunds_Failure() throws Exception { + UserDetails userDetails = userService.loadUserByUsername(userUser.getUsername()); + long userId = userService.getUserIdByUsername(userUser.getUsername()); + + Long sourceAccountId = createAccountForUserWithCurrency(userId, AccountCurrency.USD); + long withdrawAmount = 1200L; + BankingUserDetails bankingDetails = new BankingUserDetails(userUser.getId(), userUser.getUsername(), userDetails.getPassword(), false); + + Authentication authentication = new UsernamePasswordAuthenticationToken(bankingDetails, null, bankingDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + + mockMvc.perform(post("/account/withdraw/{id}", sourceAccountId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new BalanceChangeRequest(withdrawAmount)))) + .andExpect(status().isBadRequest()); + } + + private long getCurrentUserId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return userService.getUserIdByUsername(((UserDetails) authentication.getPrincipal()).getUsername()); + } + + private Long createAccountForUser(long userId) { + AccountDTO accountDTO = accountService.createTestAccount(userId, 1000L, 1L); + return accountDTO.getId(); + } + + private Long createAccountForUserWithCurrency(long userId, AccountCurrency currency) { + AccountDTO accountDTO = accountService.createAccount(userId, 1000L); + accountService.changeAccountCurrency(accountDTO.getId(), currency); + return accountDTO.getId(); + } + + + public TestRestTemplate restTemplate() { + return new TestRestTemplate(); + } +} diff --git a/src/test/java/com/skypro/simplebanking/SimpleBankingApplicationTests.java b/src/test/java/com/skypro/simplebanking/SimpleBankingApplicationTests.java index 1d4dd66..3cd5062 100644 --- a/src/test/java/com/skypro/simplebanking/SimpleBankingApplicationTests.java +++ b/src/test/java/com/skypro/simplebanking/SimpleBankingApplicationTests.java @@ -5,5 +5,7 @@ @SpringBootTest class SimpleBankingApplicationTests { - + @Test + void contextLoads() { + } } diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties new file mode 100644 index 0000000..37b00cf --- /dev/null +++ b/src/test/resources/application-test.properties @@ -0,0 +1,9 @@ +app.env=test +spring.datasource.url=jdbc:postgresql://localhost:5432/postgres +spring.datasource.username=postgres +spring.datasource.password=slavakakraft228 +spring.jpa.hibernate.ddl-auto=create +spring.jpa.open-in-view=false +app.security.admin-token=SUPER_SECRET_KEY_FROM_ADMIN + +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect \ No newline at end of file