hs-office-coopshares: add validation + RestTest for Controller + improve test data
1 files added
6 files modified
| | |
| | | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; |
| | | |
| | | import javax.persistence.EntityNotFoundException; |
| | | import javax.validation.ValidationException; |
| | | import java.util.NoSuchElementException; |
| | | import java.util.Optional; |
| | | import java.util.regex.Pattern; |
| | |
| | | return errorResponse(request, HttpStatus.BAD_REQUEST, message); |
| | | } |
| | | |
| | | @ExceptionHandler(Iban4jException.class) |
| | | @ExceptionHandler({ Iban4jException.class, ValidationException.class }) |
| | | protected ResponseEntity<CustomErrorResponse> handleIbanAndBicExceptions( |
| | | final Throwable exc, final WebRequest request) { |
| | | final var message = firstLine(NestedExceptionUtils.getMostSpecificCause(exc).getMessage()); |
| | |
| | | import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionResource; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.format.annotation.DateTimeFormat; |
| | | import org.springframework.format.annotation.DateTimeFormat.ISO; |
| | | import org.springframework.http.ResponseEntity; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; |
| | | |
| | | import javax.persistence.EntityManager; |
| | | import javax.validation.Valid; |
| | | import javax.validation.ValidationException; |
| | | import java.time.LocalDate; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.UUID; |
| | | import java.util.function.BiConsumer; |
| | | |
| | | import static java.lang.String.join; |
| | | import static net.hostsharing.hsadminng.Mapper.map; |
| | | import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionTypeResource.CANCELLATION; |
| | | import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionTypeResource.SUBSCRIPTION; |
| | | |
| | | @RestController |
| | | |
| | |
| | | @Autowired |
| | | private HsOfficeCoopSharesTransactionRepository coopSharesTransactionRepo; |
| | | |
| | | @Autowired |
| | | private EntityManager em; |
| | | |
| | | @Override |
| | | @Transactional(readOnly = true) |
| | | public ResponseEntity<List<HsOfficeCoopSharesTransactionResource>> listCoopShares( |
| | | final String currentUser, |
| | | final String assumedRoles, |
| | | final UUID membershipUuid, |
| | | final @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fromValueDate, |
| | | final @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate toValueDate) { |
| | | final @DateTimeFormat(iso = ISO.DATE) LocalDate fromValueDate, |
| | | final @DateTimeFormat(iso = ISO.DATE) LocalDate toValueDate) { |
| | | context.define(currentUser, assumedRoles); |
| | | |
| | | |
| | | final var entities = coopSharesTransactionRepo.findCoopSharesTransactionByOptionalMembershipUuidAndDateRange( |
| | | membershipUuid, |
| | | fromValueDate, |
| | | toValueDate); |
| | | |
| | | final var resources = Mapper.mapList(entities, HsOfficeCoopSharesTransactionResource.class, |
| | | COOP_SHARES_ENTITY_TO_RESOURCE_POSTMAPPER); |
| | | final var resources = Mapper.mapList(entities, HsOfficeCoopSharesTransactionResource.class); |
| | | return ResponseEntity.ok(resources); |
| | | } |
| | | |
| | |
| | | public ResponseEntity<HsOfficeCoopSharesTransactionResource> addCoopSharesTransaction( |
| | | final String currentUser, |
| | | final String assumedRoles, |
| | | @Valid final HsOfficeCoopSharesTransactionInsertResource body) { |
| | | @Valid final HsOfficeCoopSharesTransactionInsertResource requestBody) { |
| | | |
| | | context.define(currentUser, assumedRoles); |
| | | validate(requestBody); |
| | | |
| | | final var entityToSave = map( |
| | | body, |
| | | HsOfficeCoopSharesTransactionEntity.class, |
| | | COOP_SHARES_RESOURCE_TO_ENTITY_POSTMAPPER); |
| | | final var entityToSave = map(requestBody, HsOfficeCoopSharesTransactionEntity.class); |
| | | entityToSave.setUuid(UUID.randomUUID()); |
| | | |
| | | final var saved = coopSharesTransactionRepo.save(entityToSave); |
| | |
| | | .path("/api/hs/office/CoopSharesTransactions/{id}") |
| | | .buildAndExpand(entityToSave.getUuid()) |
| | | .toUri(); |
| | | final var mapped = map(saved, HsOfficeCoopSharesTransactionResource.class, |
| | | COOP_SHARES_ENTITY_TO_RESOURCE_POSTMAPPER); |
| | | final var mapped = map(saved, HsOfficeCoopSharesTransactionResource.class); |
| | | return ResponseEntity.created(uri).body(mapped); |
| | | } |
| | | |
| | | final BiConsumer<HsOfficeCoopSharesTransactionEntity, HsOfficeCoopSharesTransactionResource> COOP_SHARES_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { |
| | | // resource.setValidFrom(entity.getValidity().lower()); |
| | | // if (entity.getValidity().hasUpperBound()) { |
| | | // resource.setValidTo(entity.getValidity().upper().minusDays(1)); |
| | | // } |
| | | }; |
| | | private void validate(final HsOfficeCoopSharesTransactionInsertResource requestBody) { |
| | | final var violations = new ArrayList<String>(); |
| | | validateSubscriptionTransaction(requestBody, violations); |
| | | validateCancellationTransaction(requestBody, violations); |
| | | validateSharesCount(requestBody, violations); |
| | | if (violations.size() > 0) { |
| | | throw new ValidationException("[" + join(", ", violations) + "]"); |
| | | } |
| | | } |
| | | |
| | | final BiConsumer<HsOfficeCoopSharesTransactionInsertResource, HsOfficeCoopSharesTransactionEntity> COOP_SHARES_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { |
| | | // entity.setValidity(toPostgresDateRange(resource.getValidFrom(), resource.getValidTo())); |
| | | }; |
| | | private static void validateSubscriptionTransaction( |
| | | final HsOfficeCoopSharesTransactionInsertResource requestBody, |
| | | final ArrayList<String> violations) { |
| | | if (requestBody.getTransactionType() == SUBSCRIPTION |
| | | && requestBody.getSharesCount() < 0) { |
| | | violations.add("for %s, sharesCount must be positive but is \"%d\"".formatted( |
| | | requestBody.getTransactionType(), requestBody.getSharesCount())); |
| | | } |
| | | } |
| | | |
| | | private static void validateCancellationTransaction( |
| | | final HsOfficeCoopSharesTransactionInsertResource requestBody, |
| | | final ArrayList<String> violations) { |
| | | if (requestBody.getTransactionType() == CANCELLATION |
| | | && requestBody.getSharesCount() > 0) { |
| | | violations.add("for %s, sharesCount must be negative but is \"%d\"".formatted( |
| | | requestBody.getTransactionType(), requestBody.getSharesCount())); |
| | | } |
| | | } |
| | | |
| | | private static void validateSharesCount( |
| | | final HsOfficeCoopSharesTransactionInsertResource requestBody, |
| | | final ArrayList<String> violations) { |
| | | if (requestBody.getSharesCount() == 0) { |
| | | violations.add("sharesCount must not be 0 but is \"%d\"".formatted( |
| | | requestBody.getSharesCount())); |
| | | } |
| | | } |
| | | |
| | | } |
| | |
| | | enum: |
| | | - ADJUSTMENT |
| | | - SUBSCRIPTION |
| | | - CANCELLATION; |
| | | - CANCELLATION |
| | | |
| | | HsOfficeCoopSharesTransaction: |
| | | type: object |
| | |
| | | format: date |
| | | reference: |
| | | type: string |
| | | minLength: 6 |
| | | maxLength: 48 |
| | | comment: |
| | | type: string |
| | | required: |
| | | - membershipUuid |
| | | - transactionType |
| | | - sharesCount |
| | | - valueDate |
| | |
| | | insert |
| | | into hs_office_coopsharestransaction(uuid, membershipuuid, transactiontype, valuedate, sharecount, reference, comment) |
| | | values |
| | | (uuid_generate_v4(), membership.uuid, 'SUBSCRIPTION', '2010-03-15', 2, 'ref '||givenMembershipNumber||'-1', 'initial subscription'), |
| | | (uuid_generate_v4(), membership.uuid, 'SUBSCRIPTION', '2021-09-01', 24, 'ref '||givenMembershipNumber||'-2', 'subscibing more'), |
| | | (uuid_generate_v4(), membership.uuid, 'CANCELLATION', '2022-10-20', 12, 'ref '||givenMembershipNumber||'-3', 'cancelling some'); |
| | | (uuid_generate_v4(), membership.uuid, 'SUBSCRIPTION', '2010-03-15', 4, 'ref '||givenMembershipNumber||'-1', 'initial subscription'), |
| | | (uuid_generate_v4(), membership.uuid, 'CANCELLATION', '2021-09-01', -2, 'ref '||givenMembershipNumber||'-2', 'cancelling some'), |
| | | (uuid_generate_v4(), membership.uuid, 'ADJUSTMENT', '2022-10-20', 2, 'ref '||givenMembershipNumber||'-3', 'some adjustment'); |
| | | end; $$; |
| | | --// |
| | | |
| | |
| | | [ |
| | | { |
| | | "transactionType": "SUBSCRIPTION", |
| | | "sharesCount": 2, |
| | | "sharesCount": 4, |
| | | "valueDate": "2010-03-15", |
| | | "reference": "ref 10002-1", |
| | | "comment": "initial subscription" |
| | | }, |
| | | { |
| | | "transactionType": "SUBSCRIPTION", |
| | | "sharesCount": 24, |
| | | "transactionType": "CANCELLATION", |
| | | "sharesCount": -2, |
| | | "valueDate": "2021-09-01", |
| | | "reference": "ref 10002-2", |
| | | "comment": "subscibing more" |
| | | "comment": "cancelling some" |
| | | }, |
| | | { |
| | | "transactionType": "CANCELLATION;", |
| | | "sharesCount": 12, |
| | | "transactionType": "ADJUSTMENT", |
| | | "sharesCount": 2, |
| | | "valueDate": "2022-10-20", |
| | | "reference": "ref 10002-3", |
| | | "comment": "cancelling some" |
| | | "comment": "some adjustment" |
| | | } |
| | | ] |
| | | """)); // @formatter:on |
| | |
| | | .body("", lenientlyEquals(""" |
| | | [ |
| | | { |
| | | "transactionType": "SUBSCRIPTION", |
| | | "sharesCount": 24, |
| | | "transactionType": "CANCELLATION", |
| | | "sharesCount": -2, |
| | | "valueDate": "2021-09-01", |
| | | "reference": "ref 10002-2", |
| | | "comment": "subscibing more" |
| | | "comment": "cancelling some" |
| | | } |
| | | ] |
| | | """)); // @formatter:on |
| | |
| | | location.substring(location.lastIndexOf('/') + 1)); |
| | | assertThat(newUserUuid).isNotNull(); |
| | | } |
| | | |
| | | // TODO.test: move validation tests to a ...WebMvcTest |
| | | @Test |
| | | void globalAdmin_canNotAddCoopSharesTransactionWhenMembershipUuidIsMissing() { |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | @BeforeEach |
New file |
| | |
| | | package net.hostsharing.hsadminng.hs.office.coopshares; |
| | | |
| | | import net.hostsharing.hsadminng.context.Context; |
| | | import org.junit.jupiter.params.ParameterizedTest; |
| | | import org.junit.jupiter.params.provider.EnumSource; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; |
| | | import org.springframework.boot.test.mock.mockito.MockBean; |
| | | import org.springframework.http.MediaType; |
| | | import org.springframework.test.web.servlet.MockMvc; |
| | | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; |
| | | |
| | | import java.util.UUID; |
| | | |
| | | import static org.hamcrest.Matchers.is; |
| | | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; |
| | | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
| | | |
| | | @WebMvcTest(HsOfficeCoopSharesTransactionController.class) |
| | | class HsOfficeCoopSharesTransactionControllerRestTest { |
| | | |
| | | @Autowired |
| | | MockMvc mockMvc; |
| | | |
| | | @MockBean |
| | | Context contextMock; |
| | | |
| | | @MockBean |
| | | HsOfficeCoopSharesTransactionRepository coopSharesTransactionRepo; |
| | | |
| | | enum BadRequestTestCases { |
| | | MEMBERSHIP_UUID_MISSING( |
| | | """ |
| | | { |
| | | "transactionType": "SUBSCRIPTION", |
| | | "sharesCount": 8, |
| | | "valueDate": "2022-10-13", |
| | | "reference": "temp ref A" |
| | | } |
| | | """, |
| | | "[membershipUuid must not be null but is \"null\"]"), |
| | | |
| | | TRANSACTION_TYPE_MISSING( |
| | | """ |
| | | { |
| | | "membershipUuid": "%s", |
| | | "sharesCount": 8, |
| | | "valueDate": "2022-10-13", |
| | | "reference": "temp ref A" |
| | | } |
| | | """.formatted(UUID.randomUUID()), |
| | | "[transactionType must not be null but is \"null\"]"), |
| | | |
| | | VALUE_DATE_MISSING( |
| | | """ |
| | | { |
| | | "membershipUuid": "%s", |
| | | "transactionType": "SUBSCRIPTION", |
| | | "sharesCount": 8, |
| | | "reference": "temp ref A" |
| | | } |
| | | """.formatted(UUID.randomUUID()), |
| | | "[valueDate must not be null but is \"null\"]"), |
| | | |
| | | SHARES_COUNT_FOR_SUBSCRIPTION_MUST_BE_POSITIVE( |
| | | """ |
| | | { |
| | | "membershipUuid": "%s", |
| | | "transactionType": "SUBSCRIPTION", |
| | | "sharesCount": -1, |
| | | "valueDate": "2022-10-13", |
| | | "reference": "temp ref A" |
| | | } |
| | | """.formatted(UUID.randomUUID()), |
| | | "[for SUBSCRIPTION, sharesCount must be positive but is \"-1\"]"), |
| | | |
| | | SHARES_COUNT_FOR_CANCELLATION_MUST_BE_NEGATIVE( |
| | | """ |
| | | { |
| | | "membershipUuid": "%s", |
| | | "transactionType": "CANCELLATION", |
| | | "sharesCount": 1, |
| | | "valueDate": "2022-10-13", |
| | | "reference": "temp ref A" |
| | | } |
| | | """.formatted(UUID.randomUUID()), |
| | | "[for CANCELLATION, sharesCount must be negative but is \"1\"]"), |
| | | |
| | | SHARES_COUNT_MUST_NOT_BE_NULL( |
| | | """ |
| | | { |
| | | "membershipUuid": "%s", |
| | | "transactionType": "ADJUSTMENT", |
| | | "sharesCount": 0, |
| | | "valueDate": "2022-10-13", |
| | | "reference": "temp ref A" |
| | | } |
| | | """.formatted(UUID.randomUUID()), |
| | | "[sharesCount must not be 0 but is \"0\"]"), |
| | | |
| | | REFERENCE_MISSING( |
| | | """ |
| | | { |
| | | "membershipUuid": "%s", |
| | | "transactionType": "SUBSCRIPTION", |
| | | "sharesCount": 8, |
| | | "valueDate": "2022-10-13" |
| | | } |
| | | """.formatted(UUID.randomUUID()), |
| | | "[reference must not be null but is \"null\"]"), |
| | | |
| | | REFERENCE_TOO_SHORT( |
| | | """ |
| | | { |
| | | "membershipUuid": "%s", |
| | | "transactionType": "SUBSCRIPTION", |
| | | "sharesCount": 8, |
| | | "valueDate": "2022-10-13", |
| | | "reference": "12345" |
| | | } |
| | | """.formatted(UUID.randomUUID()), |
| | | "[reference size must be between 6 and 48 but is \"12345\"]"), |
| | | |
| | | REFERENCE_TOO_LONG( |
| | | """ |
| | | { |
| | | "membershipUuid": "%s", |
| | | "transactionType": "SUBSCRIPTION", |
| | | "sharesCount": 8, |
| | | "valueDate": "2022-10-13", |
| | | "reference": "0123456789012345678901234567890123456789012345678" |
| | | } |
| | | """.formatted(UUID.randomUUID()), |
| | | "[reference size must be between 6 and 48 but is \"0123456789012345678901234567890123456789012345678\"]"); |
| | | |
| | | private final String givenBody; |
| | | private final String expectedErrorMessage; |
| | | |
| | | BadRequestTestCases(final String givenBody, final String expectedErrorMessage) { |
| | | this.givenBody = givenBody; |
| | | this.expectedErrorMessage = expectedErrorMessage; |
| | | } |
| | | } |
| | | |
| | | @ParameterizedTest |
| | | @EnumSource(BadRequestTestCases.class) |
| | | void respondWithBadRequest(final BadRequestTestCases testCase) throws Exception { |
| | | |
| | | // when |
| | | mockMvc.perform(MockMvcRequestBuilders |
| | | .post("/api/hs/office/coopsharestransactions") |
| | | .header("current-user", "superuser-alex@hostsharing.net") |
| | | .contentType(MediaType.APPLICATION_JSON) |
| | | .content(testCase.givenBody) |
| | | .accept(MediaType.APPLICATION_JSON)) |
| | | |
| | | // then |
| | | .andExpect(status().is4xxClientError()) |
| | | .andExpect(jsonPath("status", is(400))) |
| | | .andExpect(jsonPath("error", is("Bad Request"))) |
| | | .andExpect(jsonPath("message", is(testCase.expectedErrorMessage))); |
| | | } |
| | | |
| | | } |
| | |
| | | // then |
| | | allTheseCoopSharesTransactionsAreReturned( |
| | | result, |
| | | "CoopShareTransaction(10001, 2010-03-15, SUBSCRIPTION, 2, ref 10001-1)", |
| | | "CoopShareTransaction(10001, 2021-09-01, SUBSCRIPTION, 24, ref 10001-2)", |
| | | "CoopShareTransaction(10001, 2022-10-20, CANCELLATION, 12, ref 10001-3)", |
| | | "CoopShareTransaction(10001, 2010-03-15, SUBSCRIPTION, 4, ref 10001-1)", |
| | | "CoopShareTransaction(10001, 2021-09-01, CANCELLATION, -2, ref 10001-2)", |
| | | "CoopShareTransaction(10001, 2022-10-20, ADJUSTMENT, 2, ref 10001-3)", |
| | | |
| | | "CoopShareTransaction(10002, 2010-03-15, SUBSCRIPTION, 2, ref 10002-1)", |
| | | "CoopShareTransaction(10002, 2021-09-01, SUBSCRIPTION, 24, ref 10002-2)", |
| | | "CoopShareTransaction(10002, 2022-10-20, CANCELLATION, 12, ref 10002-3)", |
| | | "CoopShareTransaction(10002, 2010-03-15, SUBSCRIPTION, 4, ref 10002-1)", |
| | | "CoopShareTransaction(10002, 2021-09-01, CANCELLATION, -2, ref 10002-2)", |
| | | "CoopShareTransaction(10002, 2022-10-20, ADJUSTMENT, 2, ref 10002-3)", |
| | | |
| | | "CoopShareTransaction(10003, 2010-03-15, SUBSCRIPTION, 2, ref 10003-1)", |
| | | "CoopShareTransaction(10003, 2021-09-01, SUBSCRIPTION, 24, ref 10003-2)", |
| | | "CoopShareTransaction(10003, 2022-10-20, CANCELLATION, 12, ref 10003-3)"); |
| | | "CoopShareTransaction(10003, 2010-03-15, SUBSCRIPTION, 4, ref 10003-1)", |
| | | "CoopShareTransaction(10003, 2021-09-01, CANCELLATION, -2, ref 10003-2)", |
| | | "CoopShareTransaction(10003, 2022-10-20, ADJUSTMENT, 2, ref 10003-3)"); |
| | | } |
| | | |
| | | @Test |
| | | public void globalAdmin_canViewCoopSharesTransactions_filteredByMembershipUuid() { |
| | | // given |
| | | context("superuser-alex@hostsharing.net"); |
| | | final var givenMembership = membershipRepo.findMembershipsByOptionalPartnerUuidAndOptionalMemberNumber(null, 10002).get(0); |
| | | final var givenMembership = membershipRepo.findMembershipsByOptionalPartnerUuidAndOptionalMemberNumber(null, 10002) |
| | | .get(0); |
| | | |
| | | // when |
| | | final var result = coopSharesTransactionRepo.findCoopSharesTransactionByOptionalMembershipUuidAndDateRange( |
| | |
| | | // then |
| | | allTheseCoopSharesTransactionsAreReturned( |
| | | result, |
| | | "CoopShareTransaction(10002, 2010-03-15, SUBSCRIPTION, 2, ref 10002-1)", |
| | | "CoopShareTransaction(10002, 2021-09-01, SUBSCRIPTION, 24, ref 10002-2)", |
| | | "CoopShareTransaction(10002, 2022-10-20, CANCELLATION, 12, ref 10002-3)"); |
| | | "CoopShareTransaction(10002, 2010-03-15, SUBSCRIPTION, 4, ref 10002-1)", |
| | | "CoopShareTransaction(10002, 2021-09-01, CANCELLATION, -2, ref 10002-2)", |
| | | "CoopShareTransaction(10002, 2022-10-20, ADJUSTMENT, 2, ref 10002-3)"); |
| | | } |
| | | |
| | | @Test |
| | | public void globalAdmin_canViewCoopSharesTransactions_filteredByMembershipUuidAndValueDateRange() { |
| | | // given |
| | | context("superuser-alex@hostsharing.net"); |
| | | final var givenMembership = membershipRepo.findMembershipsByOptionalPartnerUuidAndOptionalMemberNumber(null, 10002).get(0); |
| | | final var givenMembership = membershipRepo.findMembershipsByOptionalPartnerUuidAndOptionalMemberNumber(null, 10002) |
| | | .get(0); |
| | | |
| | | // when |
| | | final var result = coopSharesTransactionRepo.findCoopSharesTransactionByOptionalMembershipUuidAndDateRange( |
| | |
| | | // then |
| | | allTheseCoopSharesTransactionsAreReturned( |
| | | result, |
| | | "CoopShareTransaction(10002, 2021-09-01, SUBSCRIPTION, 24, ref 10002-2)"); |
| | | "CoopShareTransaction(10002, 2021-09-01, CANCELLATION, -2, ref 10002-2)"); |
| | | } |
| | | |
| | | @Test |
| | |
| | | // then: |
| | | exactlyTheseCoopSharesTransactionsAreReturned( |
| | | result, |
| | | "CoopShareTransaction(10001, 2010-03-15, SUBSCRIPTION, 2, ref 10001-1)", |
| | | "CoopShareTransaction(10001, 2021-09-01, SUBSCRIPTION, 24, ref 10001-2)", |
| | | "CoopShareTransaction(10001, 2022-10-20, CANCELLATION, 12, ref 10001-3)"); |
| | | "CoopShareTransaction(10001, 2010-03-15, SUBSCRIPTION, 4, ref 10001-1)", |
| | | "CoopShareTransaction(10001, 2021-09-01, CANCELLATION, -2, ref 10001-2)", |
| | | "CoopShareTransaction(10001, 2022-10-20, ADJUSTMENT, 2, ref 10001-3)"); |
| | | } |
| | | } |
| | | |