Michael Hoennig
2022-10-20 5f4f50a325e14fcfdce2dbbc72db1d96ad5f0073
hs-office-coopshares: add validation + RestTest for Controller + improve test data
1 files added
6 files modified
320 ■■■■ changed files
src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java 3 ●●●● patch | view | raw | blame | history
src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java 77 ●●●●● patch | view | raw | blame | history
src/main/resources/api-definition/hs-office/hs-office-coopshares-schemas.yaml 5 ●●●● patch | view | raw | blame | history
src/main/resources/db/changelog/318-hs-office-coopshares-test-data.sql 6 ●●●● patch | view | raw | blame | history
src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerAcceptanceTest.java 27 ●●●●● patch | view | raw | blame | history
src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerRestTest.java 164 ●●●●● patch | view | raw | blame | history
src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java 38 ●●●● patch | view | raw | blame | history
src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java
@@ -16,6 +16,7 @@
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;
@@ -57,7 +58,7 @@
        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());
src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java
@@ -7,19 +7,23 @@
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
@@ -31,27 +35,22 @@
    @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);
    }
@@ -60,14 +59,12 @@
    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);
@@ -77,19 +74,47 @@
                        .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()));
        }
    }
}
src/main/resources/api-definition/hs-office/hs-office-coopshares-schemas.yaml
@@ -8,7 +8,7 @@
            enum:
                - ADJUSTMENT
                - SUBSCRIPTION
                - CANCELLATION;
                - CANCELLATION
        HsOfficeCoopSharesTransaction:
            type: object
@@ -44,9 +44,12 @@
                    format: date
                reference:
                    type: string
                    minLength: 6
                    maxLength: 48
                comment:
                    type: string
            required:
                - membershipUuid
                - transactionType
                - sharesCount
                - valueDate
src/main/resources/db/changelog/318-hs-office-coopshares-test-data.sql
@@ -24,9 +24,9 @@
    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; $$;
--//
src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerAcceptanceTest.java
@@ -92,24 +92,24 @@
                        [
                            {
                                "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
@@ -135,11 +135,11 @@
                    .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
@@ -195,13 +195,6 @@
                    location.substring(location.lastIndexOf('/') + 1));
            assertThat(newUserUuid).isNotNull();
        }
        // TODO.test: move validation tests to a ...WebMvcTest
        @Test
        void globalAdmin_canNotAddCoopSharesTransactionWhenMembershipUuidIsMissing() {
        }
    }
    @BeforeEach
src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerRestTest.java
New file
@@ -0,0 +1,164 @@
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)));
    }
}
src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java
@@ -147,24 +147,25 @@
            // 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(
@@ -175,16 +176,17 @@
            // 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(
@@ -195,7 +197,7 @@
            // then
            allTheseCoopSharesTransactionsAreReturned(
                    result,
                    "CoopShareTransaction(10002, 2021-09-01, SUBSCRIPTION, 24, ref 10002-2)");
                    "CoopShareTransaction(10002, 2021-09-01, CANCELLATION, -2, ref 10002-2)");
        }
        @Test
@@ -213,9 +215,9 @@
            // 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)");
        }
    }