feat: ExchangeController & tests

This commit is contained in:
Piotr Dec 2024-05-25 00:33:33 +02:00
parent 720937bd6c
commit 95ed5f6ae7
Signed by: stawros
GPG key ID: F89F27AD8F881A91
8 changed files with 85 additions and 21 deletions

View file

@ -0,0 +1,11 @@
package eu.ztsh.wymiana.exception;
import eu.ztsh.wymiana.web.model.CurrencyExchangeRequest;
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(CurrencyExchangeRequest entity) {
super("User with PESEL %s not found".formatted(entity.pesel()));
}
}

View file

@ -2,6 +2,7 @@ package eu.ztsh.wymiana.service;
import eu.ztsh.wymiana.exception.ExchangeFailedException; import eu.ztsh.wymiana.exception.ExchangeFailedException;
import eu.ztsh.wymiana.exception.InsufficientFundsException; import eu.ztsh.wymiana.exception.InsufficientFundsException;
import eu.ztsh.wymiana.exception.UserNotFoundException;
import eu.ztsh.wymiana.model.Currency; import eu.ztsh.wymiana.model.Currency;
import eu.ztsh.wymiana.model.User; import eu.ztsh.wymiana.model.User;
import eu.ztsh.wymiana.validation.InstanceValidator; import eu.ztsh.wymiana.validation.InstanceValidator;
@ -49,7 +50,7 @@ public class CurrencyService {
user.currencies().putAll(exchanged); user.currencies().putAll(exchanged);
return userService.update(user); return userService.update(user);
}) })
.orElseThrow(ExchangeFailedException::new); .orElseThrow(() -> new UserNotFoundException(request));
} }
private Currency create(String symbol) { private Currency create(String symbol) {

View file

@ -9,9 +9,11 @@ import eu.ztsh.wymiana.web.model.UserCreateRequest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.hibernate.validator.constraints.pl.PESEL; import org.hibernate.validator.constraints.pl.PESEL;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.Optional; import java.util.Optional;
@Validated
@RequiredArgsConstructor @RequiredArgsConstructor
@Service @Service
public class UserService { public class UserService {

View file

@ -0,0 +1,42 @@
package eu.ztsh.wymiana.web.controller;
import eu.ztsh.wymiana.exception.InsufficientFundsException;
import eu.ztsh.wymiana.exception.UserAlreadyExistsException;
import eu.ztsh.wymiana.exception.UserNotFoundException;
import eu.ztsh.wymiana.service.CurrencyService;
import eu.ztsh.wymiana.validation.ValidationFailedException;
import eu.ztsh.wymiana.web.model.CurrencyExchangeRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequiredArgsConstructor
@Validated
@RestController
@RequestMapping(path = "/api/exchange", produces = "application/json")
public class ExchangeController {
private final CurrencyService currencyService;
@PostMapping
public ResponseEntity<Object> exchange(@Valid @RequestBody CurrencyExchangeRequest request) {
try {
return ResponseEntity.status(200).body(currencyService.exchange(request));
} catch (Exception e) {
var status = switch (e) {
case InsufficientFundsException ignored -> HttpStatus.BAD_REQUEST;
case UserNotFoundException ignored -> HttpStatus.NOT_FOUND;
default -> HttpStatus.INTERNAL_SERVER_ERROR;
};
return ResponseEntity.status(status).body(e.getMessage());
}
}
}

View file

@ -6,6 +6,7 @@ import eu.ztsh.wymiana.service.UserService;
import eu.ztsh.wymiana.validation.ValidationFailedException; import eu.ztsh.wymiana.validation.ValidationFailedException;
import eu.ztsh.wymiana.web.model.UserCreateRequest; import eu.ztsh.wymiana.web.model.UserCreateRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.ValidationException;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -27,7 +28,13 @@ public class UserController {
@GetMapping("{pesel}") @GetMapping("{pesel}")
public ResponseEntity<User> get(@PathVariable("pesel") String pesel) { public ResponseEntity<User> get(@PathVariable("pesel") String pesel) {
return ResponseEntity.of(userService.get(pesel)); try {
return userService.get(pesel)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
} catch (ValidationException e) {
return ResponseEntity.badRequest().build();
}
} }
@PostMapping @PostMapping

View file

@ -48,7 +48,7 @@ public class EntityCreator {
currencies.put("PLN", new Currency("PLN", pln)); currencies.put("PLN", new Currency("PLN", pln));
} }
if (usd > 0) { if (usd > 0) {
currencies.put("USD", new Currency("USD", pln)); currencies.put("USD", new Currency("USD", usd));
} }
return new User(Constants.NAME, Constants.SURNAME, Constants.PESEL, currencies); return new User(Constants.NAME, Constants.SURNAME, Constants.PESEL, currencies);
} }

View file

@ -5,6 +5,7 @@ import eu.ztsh.wymiana.RepositoryBasedTest;
import eu.ztsh.wymiana.data.repository.UserRepository; import eu.ztsh.wymiana.data.repository.UserRepository;
import eu.ztsh.wymiana.exception.ExchangeFailedException; import eu.ztsh.wymiana.exception.ExchangeFailedException;
import eu.ztsh.wymiana.exception.InsufficientFundsException; import eu.ztsh.wymiana.exception.InsufficientFundsException;
import eu.ztsh.wymiana.exception.UserNotFoundException;
import eu.ztsh.wymiana.validation.InstanceValidator; import eu.ztsh.wymiana.validation.InstanceValidator;
import eu.ztsh.wymiana.validation.ValidationFailedException; import eu.ztsh.wymiana.validation.ValidationFailedException;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
@ -175,7 +176,7 @@ class CurrencyServiceTest extends RepositoryBasedTest {
.toSell(USD_SELL) .toSell(USD_SELL)
.build(); .build();
assertThatThrownBy(() -> currencyService.exchange(entity)) assertThatThrownBy(() -> currencyService.exchange(entity))
.isInstanceOf(ExchangeFailedException.class); .isInstanceOf(UserNotFoundException.class);
} }
@Test @Test

View file

@ -4,7 +4,6 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import eu.ztsh.wymiana.EntityCreator; import eu.ztsh.wymiana.EntityCreator;
import eu.ztsh.wymiana.WireMockExtension; import eu.ztsh.wymiana.WireMockExtension;
import eu.ztsh.wymiana.model.User;
import eu.ztsh.wymiana.util.UserMapper; import eu.ztsh.wymiana.util.UserMapper;
import org.junit.jupiter.api.ClassOrderer; import org.junit.jupiter.api.ClassOrderer;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
@ -83,7 +82,7 @@ class ApplicationIntegrationTests {
.uri(endpoint) .uri(endpoint)
.bodyValue(EntityCreator.userRequest().pln(100).build()) .bodyValue(EntityCreator.userRequest().pln(100).build())
.exchange() .exchange()
.expectStatus().is2xxSuccessful(); .expectStatus().isNoContent();
} }
@Test @Test
@ -94,7 +93,7 @@ class ApplicationIntegrationTests {
.bodyValue(EntityCreator.userRequest().pesel(INVALID_PESEL).build()) .bodyValue(EntityCreator.userRequest().pesel(INVALID_PESEL).build())
.exchange() .exchange()
.expectStatus() .expectStatus()
.is4xxClientError(); .isBadRequest();
} }
@Test @Test
@ -104,7 +103,7 @@ class ApplicationIntegrationTests {
.uri(endpoint) .uri(endpoint)
.bodyValue(EntityCreator.userRequest().build()) .bodyValue(EntityCreator.userRequest().build())
.exchange() .exchange()
.expectStatus().is4xxClientError() .expectStatus().isEqualTo(409)
.expectBody(String.class).isEqualTo("User with PESEL %s already exists".formatted(PESEL)); .expectBody(String.class).isEqualTo("User with PESEL %s already exists".formatted(PESEL));
} }
@ -114,7 +113,7 @@ class ApplicationIntegrationTests {
webTestClient.get() webTestClient.get()
.uri(endpoint.concat("/").concat(PESEL)) .uri(endpoint.concat("/").concat(PESEL))
.exchange() .exchange()
.expectStatus().is2xxSuccessful() .expectStatus().isOk()
.expectBody().json(asJson(UserMapper.entityToPojo(EntityCreator.userEntity().pln(100).build()))); .expectBody().json(asJson(UserMapper.entityToPojo(EntityCreator.userEntity().pln(100).build())));
} }
@ -124,7 +123,7 @@ class ApplicationIntegrationTests {
webTestClient.get() webTestClient.get()
.uri(endpoint.concat("/").concat(ANOTHER_PESEL)) .uri(endpoint.concat("/").concat(ANOTHER_PESEL))
.exchange() .exchange()
.expectStatus().is4xxClientError(); .expectStatus().isNotFound();
} }
@Test @Test
@ -133,7 +132,7 @@ class ApplicationIntegrationTests {
webTestClient.get() webTestClient.get()
.uri(endpoint.concat("/").concat(INVALID_PESEL)) .uri(endpoint.concat("/").concat(INVALID_PESEL))
.exchange() .exchange()
.expectStatus().is4xxClientError(); .expectStatus().isBadRequest();
} }
} }
@ -158,18 +157,19 @@ class ApplicationIntegrationTests {
.toSell(PLN) .toSell(PLN)
.build()) .build())
.exchange() .exchange()
.expectStatus().is5xxServerError(); .expectStatus().isEqualTo(500);
} }
@Test @Test
@DisplayName("03.2: Perform valid money exchange") @DisplayName("03.2: Perform valid money exchange")
void exchangeTest() throws JsonProcessingException { void exchangeTest() {
var date = getTodayOrLastFriday(); var date = getTodayOrLastFriday();
WireMockExtension.response( WireMockExtension.response(
URI_PATTERN.formatted(date), URI_PATTERN.formatted(date),
200, 200,
new ObjectMapper().writeValueAsString(EntityCreator.rates(date)) asJson(EntityCreator.rates(date))
); );
var expected = asJson(EntityCreator.user(100 - PLN, USD_BUY));
webTestClient.post() webTestClient.post()
.uri(endpoint) .uri(endpoint)
.bodyValue(EntityCreator.exchangeRequest() .bodyValue(EntityCreator.exchangeRequest()
@ -178,8 +178,9 @@ class ApplicationIntegrationTests {
.toSell(PLN) .toSell(PLN)
.build()) .build())
.exchange() .exchange()
.expectStatus().is2xxSuccessful() .expectStatus().isOk()
.expectBody(User.class).isEqualTo(EntityCreator.user(100 - PLN, USD_BUY)); .expectBody().json(expected);
WireMockExtension.verifyGet(2, URI_PATTERN.formatted(date));
} }
@Test @Test
@ -194,8 +195,8 @@ class ApplicationIntegrationTests {
.toSell(PLN) .toSell(PLN)
.build()) .build())
.exchange() .exchange()
.expectStatus().is4xxClientError() .expectStatus().isNotFound()
.expectBody(String.class).isEqualTo("An exchange error has occurred"); .expectBody(String.class).isEqualTo("User with PESEL %s not found".formatted(ANOTHER_PESEL));
} }
@Test @Test
@ -210,7 +211,7 @@ class ApplicationIntegrationTests {
.toSell(PLN) .toSell(PLN)
.build()) .build())
.exchange() .exchange()
.expectStatus().is4xxClientError(); .expectStatus().isBadRequest();
} }
@Test @Test
@ -219,13 +220,12 @@ class ApplicationIntegrationTests {
webTestClient.post() webTestClient.post()
.uri(endpoint) .uri(endpoint)
.bodyValue(EntityCreator.exchangeRequest() .bodyValue(EntityCreator.exchangeRequest()
.pesel(ANOTHER_PESEL)
.from(USD_SYMBOL) .from(USD_SYMBOL)
.to(PLN_SYMBOL) .to(PLN_SYMBOL)
.toBuy(PLN) .toBuy(PLN)
.build()) .build())
.exchange() .exchange()
.expectStatus().is4xxClientError(); .expectStatus().isBadRequest();
} }
private String getTodayOrLastFriday() { private String getTodayOrLastFriday() {