1
0

OfficeScenarioTests CoopShares+Assets (#121)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/121
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
Michael Hoennig
2024-11-15 11:54:18 +01:00
parent 8f410198e9
commit c98a5acb38
56 changed files with 836 additions and 247 deletions

View File

@@ -442,7 +442,7 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
34002=CoopAssetsTransaction(M-1002000: 2016-12-31, DISBURSAL, -100.00, 1002000, for cancellation D),
34003=CoopAssetsTransaction(M-1002000: 2016-12-31, LOSS, -20.00, 1002000, for cancellation D),
35001=CoopAssetsTransaction(M-1909000: 2024-01-15, DEPOSIT, 128.00, 1909000, for subscription E),
35002=CoopAssetsTransaction(M-1909000: 2024-01-20, ADJUSTMENT, -128.00, 1909000, chargeback for subscription E, M-1909000:DEP:+128.00),
35002=CoopAssetsTransaction(M-1909000: 2024-01-20, REVERSAL, -128.00, 1909000, chargeback for subscription E, M-1909000:DEP:+128.00),
358=CoopAssetsTransaction(M-1000300: 2000-12-06, DEPOSIT, 5120, 1000300, for subscription A),
442=CoopAssetsTransaction(M-1015200: 2003-07-07, DEPOSIT, 64, 1015200),
577=CoopAssetsTransaction(M-1000300: 2011-12-12, DEPOSIT, 1024, 1000300),
@@ -795,23 +795,23 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
? HsOfficeCoopSharesTransactionType.SUBSCRIPTION
: "UNSUBSCRIPTION".equals(rec.getString("action"))
? HsOfficeCoopSharesTransactionType.CANCELLATION
: HsOfficeCoopSharesTransactionType.ADJUSTMENT
: HsOfficeCoopSharesTransactionType.REVERSAL
)
.shareCount(rec.getInteger("quantity"))
.comment(rec.getString("comment"))
.reference(member.getMemberNumber().toString())
.build();
if (shareTransaction.getTransactionType() == HsOfficeCoopSharesTransactionType.ADJUSTMENT) {
if (shareTransaction.getTransactionType() == HsOfficeCoopSharesTransactionType.REVERSAL) {
final var negativeValue = -shareTransaction.getShareCount();
final var adjustedShareTx = coopShares.values().stream().filter(a ->
a.getTransactionType() != HsOfficeCoopSharesTransactionType.ADJUSTMENT &&
final var revertedShareTx = coopShares.values().stream().filter(a ->
a.getTransactionType() != HsOfficeCoopSharesTransactionType.REVERSAL &&
a.getMembership() == shareTransaction.getMembership() &&
a.getShareCount() == negativeValue)
.findAny()
.orElseThrow(() -> new IllegalStateException(
"cannot determine share reverse entry for adjustment " + shareTransaction));
shareTransaction.setAdjustedShareTx(adjustedShareTx);
"cannot determine share reverse entry for reversal " + shareTransaction));
shareTransaction.setRevertedShareTx(revertedShareTx);
}
coopShares.put(rec.getInteger("member_share_id"), shareTransaction);
});
@@ -837,7 +837,7 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
final var assetTypeMapping = new HashMap<String, HsOfficeCoopAssetsTransactionType>() {
{
put("ADJUSTMENT", HsOfficeCoopAssetsTransactionType.ADJUSTMENT);
put("ADJUSTMENT", HsOfficeCoopAssetsTransactionType.REVERSAL);
put("HANDOVER", HsOfficeCoopAssetsTransactionType.TRANSFER);
put("ADOPTION", HsOfficeCoopAssetsTransactionType.ADOPTION);
put("LOSS", HsOfficeCoopAssetsTransactionType.LOSS);
@@ -865,16 +865,16 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
.reference(member.getMemberNumber().toString())
.build();
if (assetTransaction.getTransactionType() == HsOfficeCoopAssetsTransactionType.ADJUSTMENT) {
if (assetTransaction.getTransactionType() == HsOfficeCoopAssetsTransactionType.REVERSAL) {
final var negativeValue = assetTransaction.getAssetValue().negate();
final var adjustedAssetTx = coopAssets.values().stream().filter(a ->
a.getTransactionType() != HsOfficeCoopAssetsTransactionType.ADJUSTMENT &&
final var revertedAssetTx = coopAssets.values().stream().filter(a ->
a.getTransactionType() != HsOfficeCoopAssetsTransactionType.REVERSAL &&
a.getMembership() == assetTransaction.getMembership() &&
a.getAssetValue().equals(negativeValue))
.findAny()
.orElseThrow(() -> new IllegalStateException(
"cannot determine asset reverse entry for adjustment " + assetTransaction));
assetTransaction.setAdjustedAssetTx(adjustedAssetTx);
"cannot determine asset reverse entry for reversal " + assetTransaction));
assetTransaction.setRevertedAssetTx(revertedAssetTx);
}
coopAssets.put(rec.getInteger("member_asset_id"), assetTransaction);

View File

@@ -55,7 +55,7 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased
EntityManager em;
@Nested
class ListCoopAssetsTransactions {
class GetListOfCoopAssetsTransactions {
@Test
void globalAdmin_canViewAllCoopAssetsTransactions() {
@@ -109,21 +109,21 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased
"valueDate": "2022-10-20",
"reference": "ref 1000202-3",
"comment": "some loss",
"adjustmentAssetTx": {
"transactionType": "ADJUSTMENT",
"reversalAssetTx": {
"transactionType": "REVERSAL",
"assetValue": -128.00,
"valueDate": "2022-10-21",
"reference": "ref 1000202-3",
"comment": "some adjustment"
"comment": "some reversal"
}
},
{
"transactionType": "ADJUSTMENT",
"transactionType": "REVERSAL",
"assetValue": -128.00,
"valueDate": "2022-10-21",
"reference": "ref 1000202-3",
"comment": "some adjustment",
"adjustedAssetTx": {
"comment": "some reversal",
"revertedAssetTx": {
"transactionType": "DEPOSIT",
"assetValue": 128.00,
"valueDate": "2022-10-20",
@@ -166,10 +166,10 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased
}
@Nested
class AddCoopAssetsTransaction {
class PostNewCoopAssetTransaction {
@Test
void globalAdmin_canAddCoopAssetsTransaction() {
void globalAdmin_canPostNewCoopAssetTransaction() {
context.define("superuser-alex@hostsharing.net");
final var givenMembership = membershipRepo.findMembershipByMemberNumber(1000101);
@@ -214,7 +214,7 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased
}
@Test
void globalAdmin_canAddCoopAssetsAdjustmentTransaction() {
void globalAdmin_canAddCoopAssetsReversalTransaction() {
context.define("superuser-alex@hostsharing.net");
final var givenMembership = membershipRepo.findMembershipByMemberNumber(1000101);
@@ -238,12 +238,12 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased
.body("""
{
"membership.uuid": "%s",
"transactionType": "ADJUSTMENT",
"transactionType": "REVERSAL",
"assetValue": %s,
"valueDate": "2022-10-30",
"reference": "test ref adjustment",
"comment": "some coop assets adjustment transaction",
"reverseEntry.uuid": "%s"
"reference": "test ref reversal",
"comment": "some coop assets reversal transaction",
"revertedAssetTx.uuid": "%s"
}
""".formatted(
givenMembership.getUuid(),
@@ -258,12 +258,12 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased
.body("uuid", isUuidValid())
.body("", lenientlyEquals("""
{
"transactionType": "ADJUSTMENT",
"transactionType": "REVERSAL",
"assetValue": -256.00,
"valueDate": "2022-10-30",
"reference": "test ref adjustment",
"comment": "some coop assets adjustment transaction",
"adjustedAssetTx": {
"reference": "test ref reversal",
"comment": "some coop assets reversal transaction",
"revertedAssetTx": {
"transactionType": "DEPOSIT",
"assetValue": 256.00,
"valueDate": "2022-10-20",

View File

@@ -77,7 +77,7 @@ class HsOfficeCoopAssetsTransactionControllerRestTest {
ASSETS_VALUE_MUST_NOT_BE_NULL(
requestBody -> requestBody
.with("transactionType", "ADJUSTMENT")
.with("transactionType", "REVERSAL")
.with("assetValue", 0.00),
"[assetValue must not be 0 but is \"0.00\"]"),

View File

@@ -21,14 +21,14 @@ class HsOfficeCoopAssetsTransactionEntityUnitTest {
.build();
final HsOfficeCoopAssetsTransactionEntity givenCoopAssetAdjustmentTransaction = HsOfficeCoopAssetsTransactionEntity.builder()
final HsOfficeCoopAssetsTransactionEntity givenCoopAssetReversalTransaction = HsOfficeCoopAssetsTransactionEntity.builder()
.membership(TEST_MEMBERSHIP)
.reference("some-ref")
.valueDate(LocalDate.parse("2020-01-15"))
.transactionType(HsOfficeCoopAssetsTransactionType.ADJUSTMENT)
.transactionType(HsOfficeCoopAssetsTransactionType.REVERSAL)
.assetValue(new BigDecimal("-128.00"))
.comment("some comment")
.adjustedAssetTx(givenCoopAssetTransaction)
.revertedAssetTx(givenCoopAssetTransaction)
.build();
final HsOfficeCoopAssetsTransactionEntity givenEmptyCoopAssetsTransaction = HsOfficeCoopAssetsTransactionEntity.builder().build();
@@ -41,12 +41,12 @@ class HsOfficeCoopAssetsTransactionEntityUnitTest {
}
@Test
void toStringWithReverseEntryContainsReverseEntry() {
givenCoopAssetTransaction.setAdjustedAssetTx(givenCoopAssetAdjustmentTransaction);
void toStringWithRevertedAssetTxContainsRevertedAssetTx() {
givenCoopAssetTransaction.setRevertedAssetTx(givenCoopAssetReversalTransaction);
final var result = givenCoopAssetTransaction.toString();
assertThat(result).isEqualTo("CoopAssetsTransaction(M-1000101: 2020-01-01, DEPOSIT, 128.00, some-ref, some comment, M-1000101:ADJ:-128.00)");
assertThat(result).isEqualTo("CoopAssetsTransaction(M-1000101: 2020-01-01, DEPOSIT, 128.00, some-ref, some comment, M-1000101:REV:-128.00)");
}
@Test

View File

@@ -69,7 +69,7 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
final var newCoopAssetsTransaction = HsOfficeCoopAssetsTransactionEntity.builder()
.membership(givenMembership)
.transactionType(HsOfficeCoopAssetsTransactionType.DEPOSIT)
.assetValue(new BigDecimal("128.00"))
.assetValue(new BigDecimal("6400.00"))
.valueDate(LocalDate.parse("2022-10-18"))
.reference("temp ref A")
.build();
@@ -98,7 +98,7 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
final var newCoopAssetsTransaction = HsOfficeCoopAssetsTransactionEntity.builder()
.membership(givenMembership)
.transactionType(HsOfficeCoopAssetsTransactionType.DEPOSIT)
.assetValue(new BigDecimal("128.00"))
.assetValue(new BigDecimal("6400.00"))
.valueDate(LocalDate.parse("2022-10-18"))
.reference("temp ref B")
.build();
@@ -142,18 +142,18 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
result,
"CoopAssetsTransaction(M-1000101: 2010-03-15, DEPOSIT, 320.00, ref 1000101-1, initial deposit)",
"CoopAssetsTransaction(M-1000101: 2021-09-01, DISBURSAL, -128.00, ref 1000101-2, partial disbursal)",
"CoopAssetsTransaction(M-1000101: 2022-10-20, DEPOSIT, 128.00, ref 1000101-3, some loss, M-1000101:ADJ:-128.00)",
"CoopAssetsTransaction(M-1000101: 2022-10-21, ADJUSTMENT, -128.00, ref 1000101-3, some adjustment, M-1000101:DEP:+128.00)",
"CoopAssetsTransaction(M-1000101: 2022-10-20, DEPOSIT, 128.00, ref 1000101-3, some loss, M-1000101:REV:-128.00)",
"CoopAssetsTransaction(M-1000101: 2022-10-21, REVERSAL, -128.00, ref 1000101-3, some reversal, M-1000101:DEP:+128.00)",
"CoopAssetsTransaction(M-1000202: 2010-03-15, DEPOSIT, 320.00, ref 1000202-1, initial deposit)",
"CoopAssetsTransaction(M-1000202: 2021-09-01, DISBURSAL, -128.00, ref 1000202-2, partial disbursal)",
"CoopAssetsTransaction(M-1000202: 2022-10-20, DEPOSIT, 128.00, ref 1000202-3, some loss, M-1000202:ADJ:-128.00)",
"CoopAssetsTransaction(M-1000202: 2022-10-21, ADJUSTMENT, -128.00, ref 1000202-3, some adjustment, M-1000202:DEP:+128.00)",
"CoopAssetsTransaction(M-1000202: 2022-10-20, DEPOSIT, 128.00, ref 1000202-3, some loss, M-1000202:REV:-128.00)",
"CoopAssetsTransaction(M-1000202: 2022-10-21, REVERSAL, -128.00, ref 1000202-3, some reversal, M-1000202:DEP:+128.00)",
"CoopAssetsTransaction(M-1000303: 2010-03-15, DEPOSIT, 320.00, ref 1000303-1, initial deposit)",
"CoopAssetsTransaction(M-1000303: 2021-09-01, DISBURSAL, -128.00, ref 1000303-2, partial disbursal)",
"CoopAssetsTransaction(M-1000303: 2022-10-20, DEPOSIT, 128.00, ref 1000303-3, some loss, M-1000303:ADJ:-128.00)",
"CoopAssetsTransaction(M-1000303: 2022-10-21, ADJUSTMENT, -128.00, ref 1000303-3, some adjustment, M-1000303:DEP:+128.00)");
"CoopAssetsTransaction(M-1000303: 2022-10-20, DEPOSIT, 128.00, ref 1000303-3, some loss, M-1000303:REV:-128.00)",
"CoopAssetsTransaction(M-1000303: 2022-10-21, REVERSAL, -128.00, ref 1000303-3, some reversal, M-1000303:DEP:+128.00)");
}
@Test
@@ -173,8 +173,8 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
result,
"CoopAssetsTransaction(M-1000202: 2010-03-15, DEPOSIT, 320.00, ref 1000202-1, initial deposit)",
"CoopAssetsTransaction(M-1000202: 2021-09-01, DISBURSAL, -128.00, ref 1000202-2, partial disbursal)",
"CoopAssetsTransaction(M-1000202: 2022-10-20, DEPOSIT, 128.00, ref 1000202-3, some loss, M-1000202:ADJ:-128.00)",
"CoopAssetsTransaction(M-1000202: 2022-10-21, ADJUSTMENT, -128.00, ref 1000202-3, some adjustment, M-1000202:DEP:+128.00)");
"CoopAssetsTransaction(M-1000202: 2022-10-20, DEPOSIT, 128.00, ref 1000202-3, some loss, M-1000202:REV:-128.00)",
"CoopAssetsTransaction(M-1000202: 2022-10-21, REVERSAL, -128.00, ref 1000202-3, some reversal, M-1000202:DEP:+128.00)");
}
@Test
@@ -211,8 +211,8 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
result,
"CoopAssetsTransaction(M-1000101: 2010-03-15, DEPOSIT, 320.00, ref 1000101-1, initial deposit)",
"CoopAssetsTransaction(M-1000101: 2021-09-01, DISBURSAL, -128.00, ref 1000101-2, partial disbursal)",
"CoopAssetsTransaction(M-1000101: 2022-10-20, DEPOSIT, 128.00, ref 1000101-3, some loss, M-1000101:ADJ:-128.00)",
"CoopAssetsTransaction(M-1000101: 2022-10-21, ADJUSTMENT, -128.00, ref 1000101-3, some adjustment, M-1000101:DEP:+128.00)");
"CoopAssetsTransaction(M-1000101: 2022-10-20, DEPOSIT, 128.00, ref 1000101-3, some loss, M-1000101:REV:-128.00)",
"CoopAssetsTransaction(M-1000101: 2022-10-21, REVERSAL, -128.00, ref 1000101-3, some reversal, M-1000101:DEP:+128.00)");
}
}

View File

@@ -62,7 +62,7 @@ class HsOfficeCoopSharesTransactionControllerAcceptanceTest extends ContextBased
}
@Nested
class ListCoopSharesTransactions {
class getListOfCoopSharesTransactions {
@Test
void globalAdmin_canViewAllCoopSharesTransactions() {
@@ -108,21 +108,21 @@ class HsOfficeCoopSharesTransactionControllerAcceptanceTest extends ContextBased
"valueDate": "2022-10-20",
"reference": "ref 1000202-3",
"comment": "some subscription",
"adjustmentShareTx": {
"transactionType": "ADJUSTMENT",
"reversalShareTx": {
"transactionType": "REVERSAL",
"shareCount": -2,
"valueDate": "2022-10-21",
"reference": "ref 1000202-4",
"comment": "some adjustment"
"comment": "some reversal"
}
},
{
"transactionType": "ADJUSTMENT",
"transactionType": "REVERSAL",
"shareCount": -2,
"valueDate": "2022-10-21",
"reference": "ref 1000202-4",
"comment": "some adjustment",
"adjustedShareTx": {
"comment": "some reversal",
"revertedShareTx": {
"transactionType": "SUBSCRIPTION",
"shareCount": 2,
"valueDate": "2022-10-20",
@@ -191,7 +191,7 @@ class HsOfficeCoopSharesTransactionControllerAcceptanceTest extends ContextBased
}
@Test
void globalAdmin_canAddCoopSharesAdjustmentTransaction() {
void globalAdmin_canAddCoopSharesReversalTransaction() {
context.define("superuser-alex@hostsharing.net");
final var givenMembership = membershipRepo.findMembershipByMemberNumber(1000101);
@@ -213,16 +213,16 @@ class HsOfficeCoopSharesTransactionControllerAcceptanceTest extends ContextBased
.header("current-subject", "superuser-alex@hostsharing.net")
.contentType(ContentType.JSON)
.body("""
{
"membership.uuid": "%s",
"transactionType": "ADJUSTMENT",
"shareCount": %s,
"valueDate": "2022-10-30",
"reference": "test ref adjustment",
"comment": "some coop shares adjustment transaction",
"adjustedShareTx.uuid": "%s"
}
""".formatted(
{
"membership.uuid": "%s",
"transactionType": "REVERSAL",
"shareCount": %s,
"valueDate": "2022-10-30",
"reference": "test reversal ref",
"comment": "some coop shares reversal transaction",
"revertedShareTx.uuid": "%s"
}
""".formatted(
givenMembership.getUuid(),
-givenTransaction.getShareCount(),
givenTransaction.getUuid()))
@@ -235,12 +235,12 @@ class HsOfficeCoopSharesTransactionControllerAcceptanceTest extends ContextBased
.body("uuid", isUuidValid())
.body("", lenientlyEquals("""
{
"transactionType": "ADJUSTMENT",
"transactionType": "REVERSAL",
"shareCount": -13,
"valueDate": "2022-10-30",
"reference": "test ref adjustment",
"comment": "some coop shares adjustment transaction",
"adjustedShareTx": {
"reference": "test reversal ref",
"comment": "some coop shares reversal transaction",
"revertedShareTx": {
"transactionType": "SUBSCRIPTION",
"shareCount": 13,
"valueDate": "2022-10-20",

View File

@@ -73,7 +73,7 @@ class HsOfficeCoopSharesTransactionControllerRestTest {
SHARES_COUNT_MUST_NOT_BE_NULL(
requestBody -> requestBody
.with("transactionType", "ADJUSTMENT")
.with("transactionType", "REVERSAL")
.with("shareCount", 0),
"[shareCount must not be 0 but is \"0\"]"),

View File

@@ -20,14 +20,14 @@ class HsOfficeCoopSharesTransactionEntityUnitTest {
.build();
final HsOfficeCoopSharesTransactionEntity givenCoopShareAdjustmentTransaction = HsOfficeCoopSharesTransactionEntity.builder()
final HsOfficeCoopSharesTransactionEntity givenCoopShareReversalTransaction = HsOfficeCoopSharesTransactionEntity.builder()
.membership(TEST_MEMBERSHIP)
.reference("some-ref")
.valueDate(LocalDate.parse("2020-01-15"))
.transactionType(HsOfficeCoopSharesTransactionType.ADJUSTMENT)
.transactionType(HsOfficeCoopSharesTransactionType.REVERSAL)
.shareCount(-4)
.comment("some comment")
.adjustedShareTx(givenCoopSharesTransaction)
.revertedShareTx(givenCoopSharesTransaction)
.build();
final HsOfficeCoopSharesTransactionEntity givenEmptyCoopSharesTransaction = HsOfficeCoopSharesTransactionEntity.builder().build();
@@ -40,12 +40,12 @@ class HsOfficeCoopSharesTransactionEntityUnitTest {
}
@Test
void toStringWithReverseEntryContainsReverseEntry() {
givenCoopSharesTransaction.setAdjustedShareTx(givenCoopShareAdjustmentTransaction);
void toStringWithRevertedAssetTxContainsRevertedAssetTx() {
givenCoopSharesTransaction.setRevertedShareTx(givenCoopShareReversalTransaction);
final var result = givenCoopSharesTransaction.toString();
assertThat(result).isEqualTo("CoopShareTransaction(M-1000101: 2020-01-01, SUBSCRIPTION, 4, some-ref, some comment, M-1000101:ADJ:-4)");
assertThat(result).isEqualTo("CoopShareTransaction(M-1000101: 2020-01-01, SUBSCRIPTION, 4, some-ref, some comment, M-1000101:REV:-4)");
}
@Test

View File

@@ -141,18 +141,18 @@ class HsOfficeCoopSharesTransactionRepositoryIntegrationTest extends ContextBase
result,
"CoopShareTransaction(M-1000101: 2010-03-15, SUBSCRIPTION, 4, ref 1000101-1, initial subscription)",
"CoopShareTransaction(M-1000101: 2021-09-01, CANCELLATION, -2, ref 1000101-2, cancelling some)",
"CoopShareTransaction(M-1000101: 2022-10-20, SUBSCRIPTION, 2, ref 1000101-3, some subscription, M-1000101:ADJ:-2)",
"CoopShareTransaction(M-1000101: 2022-10-21, ADJUSTMENT, -2, ref 1000101-4, some adjustment, M-1000101:SUB:+2)",
"CoopShareTransaction(M-1000101: 2022-10-20, SUBSCRIPTION, 2, ref 1000101-3, some subscription, M-1000101:REV:-2)",
"CoopShareTransaction(M-1000101: 2022-10-21, REVERSAL, -2, ref 1000101-4, some reversal, M-1000101:SUB:+2)",
"CoopShareTransaction(M-1000202: 2010-03-15, SUBSCRIPTION, 4, ref 1000202-1, initial subscription)",
"CoopShareTransaction(M-1000202: 2021-09-01, CANCELLATION, -2, ref 1000202-2, cancelling some)",
"CoopShareTransaction(M-1000202: 2022-10-20, SUBSCRIPTION, 2, ref 1000202-3, some subscription, M-1000202:ADJ:-2)",
"CoopShareTransaction(M-1000202: 2022-10-21, ADJUSTMENT, -2, ref 1000202-4, some adjustment, M-1000202:SUB:+2)",
"CoopShareTransaction(M-1000202: 2022-10-20, SUBSCRIPTION, 2, ref 1000202-3, some subscription, M-1000202:REV:-2)",
"CoopShareTransaction(M-1000202: 2022-10-21, REVERSAL, -2, ref 1000202-4, some reversal, M-1000202:SUB:+2)",
"CoopShareTransaction(M-1000303: 2010-03-15, SUBSCRIPTION, 4, ref 1000303-1, initial subscription)",
"CoopShareTransaction(M-1000303: 2021-09-01, CANCELLATION, -2, ref 1000303-2, cancelling some)",
"CoopShareTransaction(M-1000303: 2022-10-20, SUBSCRIPTION, 2, ref 1000303-3, some subscription, M-1000303:ADJ:-2)",
"CoopShareTransaction(M-1000303: 2022-10-21, ADJUSTMENT, -2, ref 1000303-4, some adjustment, M-1000303:SUB:+2)");
"CoopShareTransaction(M-1000303: 2022-10-20, SUBSCRIPTION, 2, ref 1000303-3, some subscription, M-1000303:REV:-2)",
"CoopShareTransaction(M-1000303: 2022-10-21, REVERSAL, -2, ref 1000303-4, some reversal, M-1000303:SUB:+2)");
}
@Test
@@ -172,8 +172,8 @@ class HsOfficeCoopSharesTransactionRepositoryIntegrationTest extends ContextBase
result,
"CoopShareTransaction(M-1000202: 2010-03-15, SUBSCRIPTION, 4, ref 1000202-1, initial subscription)",
"CoopShareTransaction(M-1000202: 2021-09-01, CANCELLATION, -2, ref 1000202-2, cancelling some)",
"CoopShareTransaction(M-1000202: 2022-10-20, SUBSCRIPTION, 2, ref 1000202-3, some subscription, M-1000202:ADJ:-2)",
"CoopShareTransaction(M-1000202: 2022-10-21, ADJUSTMENT, -2, ref 1000202-4, some adjustment, M-1000202:SUB:+2)");
"CoopShareTransaction(M-1000202: 2022-10-20, SUBSCRIPTION, 2, ref 1000202-3, some subscription, M-1000202:REV:-2)",
"CoopShareTransaction(M-1000202: 2022-10-21, REVERSAL, -2, ref 1000202-4, some reversal, M-1000202:SUB:+2)");
}
@Test
@@ -210,8 +210,8 @@ class HsOfficeCoopSharesTransactionRepositoryIntegrationTest extends ContextBase
result,
"CoopShareTransaction(M-1000101: 2010-03-15, SUBSCRIPTION, 4, ref 1000101-1, initial subscription)",
"CoopShareTransaction(M-1000101: 2021-09-01, CANCELLATION, -2, ref 1000101-2, cancelling some)",
"CoopShareTransaction(M-1000101: 2022-10-20, SUBSCRIPTION, 2, ref 1000101-3, some subscription, M-1000101:ADJ:-2)",
"CoopShareTransaction(M-1000101: 2022-10-21, ADJUSTMENT, -2, ref 1000101-4, some adjustment, M-1000101:SUB:+2)");
"CoopShareTransaction(M-1000101: 2022-10-20, SUBSCRIPTION, 2, ref 1000101-3, some subscription, M-1000101:REV:-2)",
"CoopShareTransaction(M-1000101: 2022-10-21, REVERSAL, -2, ref 1000101-4, some reversal, M-1000101:SUB:+2)");
}
}

View File

@@ -20,6 +20,7 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.util.UUID;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@@ -44,8 +45,36 @@ public class HsOfficeMembershipControllerRestTest {
@MockBean
EntityManagerWrapper em;
@Nested
class GetMemberships {
@Test
void findMembershipByNonExistingMemberNumberReturnsEmptyList() throws Exception {
// when
mockMvc.perform(MockMvcRequestBuilders
.get("/api/hs/office/memberships?memberNumber=12345")
.header("current-subject", "superuser-alex@hostsharing.net")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"partner.uuid": null,
"memberNumberSuffix": "01",
"validFrom": "2022-10-13",
"membershipFeeBillable": "true"
}
""")
.accept(MediaType.APPLICATION_JSON))
// then
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("$", hasSize(0)));
}
}
@Nested
class AddMembership {
@Test
void respondBadRequest_ifPartnerUuidIsMissing() throws Exception {
@@ -98,7 +127,9 @@ public class HsOfficeMembershipControllerRestTest {
.andExpect(status().is4xxClientError())
.andExpect(jsonPath("statusCode", is(400)))
.andExpect(jsonPath("statusPhrase", is("Bad Request")))
.andExpect(jsonPath("message", is("ERROR: [400] Unable to find Partner by partner.uuid: " + givenPartnerUuid)));
.andExpect(jsonPath(
"message",
is("ERROR: [400] Unable to find Partner by partner.uuid: " + givenPartnerUuid)));
}
@ParameterizedTest

View File

@@ -13,6 +13,12 @@ import net.hostsharing.hsadminng.hs.office.scenarios.debitor.DontDeleteDefaultDe
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.InvalidateSepaMandateForDebitor;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.CancelMembership;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.CreateMembership;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsDepositTransaction;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsDisbursalTransaction;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsRevertTransaction;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares.CreateCoopSharesCancellationTransaction;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares.CreateCoopSharesRevertTransaction;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares.CreateCoopSharesSubscriptionTransaction;
import net.hostsharing.hsadminng.hs.office.scenarios.partner.AddOperationsContactToPartner;
import net.hostsharing.hsadminng.hs.office.scenarios.partner.CreatePartner;
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.DeleteDebitor;
@@ -49,7 +55,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@Order(1010)
@Produces(explicitly = "Partner: Test AG", implicitly = {"Person: Test AG", "Contact: Test AG - Hamburg"})
@Produces(explicitly = "Partner: P-31010 - Test AG", implicitly = {"Person: Test AG", "Contact: Test AG - Hamburg"})
void shouldCreateLegalPersonAsPartner() {
new CreatePartner(this)
.given("partnerNumber", 31010)
@@ -71,7 +77,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@Order(1011)
@Produces(explicitly = "Partner: Michelle Matthieu", implicitly = {"Person: Michelle Matthieu", "Contact: Michelle Matthieu"})
@Produces(explicitly = "Partner: P-31011 - Michelle Matthieu", implicitly = {"Person: Michelle Matthieu", "Contact: Michelle Matthieu"})
void shouldCreateNaturalPersonAsPartner() {
new CreatePartner(this)
.given("partnerNumber", 31011)
@@ -148,7 +154,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@Order(1100)
@Requires("Partner: Michelle Matthieu")
@Requires("Partner: P-31011 - Michelle Matthieu")
void shouldAmendContactData() {
new AmendContactData(this)
.given("partnerName", "Matthieu")
@@ -158,7 +164,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@Order(1101)
@Requires("Partner: Michelle Matthieu")
@Requires("Partner: P-31011 - Michelle Matthieu")
void shouldAddPhoneNumberToContactData() {
new AddPhoneNumberToContactData(this)
.given("partnerName", "Matthieu")
@@ -169,7 +175,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@Order(1102)
@Requires("Partner: Michelle Matthieu")
@Requires("Partner: P-31011 - Michelle Matthieu")
void shouldRemovePhoneNumberFromContactData() {
new RemovePhoneNumberFromContactData(this)
.given("partnerName", "Matthieu")
@@ -179,7 +185,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@Order(1103)
@Requires("Partner: Test AG")
@Requires("Partner: P-31010 - Test AG")
void shouldReplaceContactData() {
new ReplaceContactData(this)
.given("partnerName", "Test AG")
@@ -201,7 +207,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@Order(1201)
@Requires("Partner: Michelle Matthieu")
@Requires("Partner: P-31011 - Michelle Matthieu")
void shouldUpdatePersonData() {
new ShouldUpdatePersonData(this)
.given("oldFamilyName", "Matthieu")
@@ -211,7 +217,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@Order(2010)
@Requires("Partner: Test AG")
@Requires("Partner: P-31010 - Test AG")
@Produces("Debitor: Test AG - main debitor")
void shouldCreateSelfDebitorForPartner() {
new CreateSelfDebitorForPartner(this, "Debitor: Test AG - main debitor")
@@ -261,18 +267,18 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@Order(2020)
@Requires("Debitor: Test AG - main debitor")
@Requires("Debitor: D-3101000 - Test AG - main debitor")
@Disabled("see TODO.spec in DontDeleteDefaultDebitor")
void shouldNotDeleteDefaultDebitor() {
new DontDeleteDefaultDebitor(this)
.given("partnerNumber", 31020)
.given("partnerNumber", 31010)
.given("debitorSuffix", "00")
.doRun();
}
@Test
@Order(3100)
@Requires("Debitor: Test AG - main debitor")
@Requires("Debitor: D-3101000 - Test AG - main debitor")
@Produces("SEPA-Mandate: Test AG")
void shouldCreateSepaMandateForDebitor() {
new CreateSepaMandateForDebitor(this)
@@ -313,12 +319,11 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@Order(4000)
@Requires("Partner: Test AG")
@Produces("Membership: Test AG 00")
@Requires("Partner: P-31010 - Test AG")
@Produces("Membership: M-3101000 - Test AG")
void shouldCreateMembershipForPartner() {
new CreateMembership(this)
.given("partnerName", "Test AG")
.given("memberNumberSuffix", "00")
.given("validFrom", "2024-10-15")
.given("newStatus", "ACTIVE")
.given("membershipFeeBillable", "true")
@@ -326,9 +331,87 @@ class HsOfficeScenarioTests extends ScenarioTest {
.keep();
}
@Test
@Order(4201)
@Requires("Membership: M-3101000 - Test AG")
@Produces("Coop-Shares SUBSCRIPTION Transaction")
void shouldSubscribeCoopShares() {
new CreateCoopSharesSubscriptionTransaction(this)
.given("memberNumber", "3101000")
.given("reference", "sign 2024-01-15")
.given("shareCount", 100)
.given("comment", "Signing the Membership")
.given("transactionDate", "2024-01-15")
.doRun();
}
@Test
@Order(4202)
@Requires("Membership: M-3101000 - Test AG")
void shouldRevertCoopSharesSubscription() {
new CreateCoopSharesRevertTransaction(this)
.given("memberNumber", "3101000")
.given("comment", "reverting some incorrect transaction")
.given("dateOfIncorrectTransaction", "2024-02-15")
.doRun();
}
@Test
@Order(4202)
@Requires("Coop-Shares SUBSCRIPTION Transaction")
@Produces("Coop-Shares CANCELLATION Transaction")
void shouldCancelCoopSharesSubscription() {
new CreateCoopSharesCancellationTransaction(this)
.given("memberNumber", "3101000")
.given("reference", "cancel 2024-01-15")
.given("sharesToCancel", 8)
.given("comment", "Cancelling 8 Shares")
.given("transactionDate", "2024-02-15")
.doRun();
}
@Test
@Order(4301)
@Requires("Membership: M-3101000 - Test AG")
@Produces("Coop-Assets DEPOSIT Transaction")
void shouldSubscribeCoopAssets() {
new CreateCoopAssetsDepositTransaction(this)
.given("memberNumber", "3101000")
.given("reference", "sign 2024-01-15")
.given("assetValue", 100*64)
.given("comment", "disposal for initial shares")
.given("transactionDate", "2024-01-15")
.doRun();
}
@Test
@Order(4302)
@Requires("Membership: M-3101000 - Test AG")
void shouldRevertCoopAssetsSubscription() {
new CreateCoopAssetsRevertTransaction(this)
.given("memberNumber", "3101000")
.given("comment", "reverting some incorrect transaction")
.given("dateOfIncorrectTransaction", "2024-02-15")
.doRun();
}
@Test
@Order(4302)
@Requires("Coop-Assets DEPOSIT Transaction")
@Produces("Coop-Assets DISBURSAL Transaction")
void shouldDisburseCoopAssets() {
new CreateCoopAssetsDisbursalTransaction(this)
.given("memberNumber", "3101000")
.given("reference", "cancel 2024-01-15")
.given("valueToDisburse", 8*64)
.given("comment", "disbursal according to shares cancellation")
.given("transactionDate", "2024-02-15")
.doRun();
}
@Test
@Order(4900)
@Requires("Membership: Test AG 00")
@Requires("Membership: M-3101000 - Test AG")
void shouldCancelMembershipOfPartner() {
new CancelMembership(this)
.given("memberNumber", "3101000")

View File

@@ -4,6 +4,9 @@ import net.hostsharing.hsadminng.hs.office.scenarios.UseCase.HttpResponse;
import java.util.function.Consumer;
import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.DROP_COMMENTS;
import static org.junit.jupiter.api.Assertions.fail;
public class PathAssertion {
private final String path;
@@ -14,10 +17,35 @@ public class PathAssertion {
@SuppressWarnings({ "unchecked", "rawtypes" })
public Consumer<UseCase.HttpResponse> contains(final String resolvableValue) {
return response -> response.path(path).contains(ScenarioTest.resolve(resolvableValue));
return response -> {
try {
response.path(path).map(this::asString).contains(ScenarioTest.resolve(resolvableValue, DROP_COMMENTS));
} catch (final AssertionError e) {
// without this, the error message is often lacking important context
fail(e.getMessage() + " in `path(\"" + path + "\").contains(\"" + resolvableValue + "\")`" );
}
};
}
public Consumer<HttpResponse> doesNotExist() {
return response -> response.path(path).isNull(); // here, null Optional means key not found in JSON
return response -> {
try {
response.path(path).isNull(); // here, null Optional means key not found in JSON
} catch (final AssertionError e) {
// without this, the error message is often lacking important context
fail(e.getMessage() + " in `path(\"" + path + "\").doesNotExist()`" );
}
};
}
private String asString(final Object value) {
if (value instanceof Double doubleValue) {
if (doubleValue % 1 == 0) {
return String.valueOf(doubleValue.intValue()); // avoid trailing ".0"
} else {
return doubleValue.toString();
}
}
return value.toString();
}
}

View File

@@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.office.scenarios;
import lombok.SneakyThrows;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository;
import net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver;
import net.hostsharing.hsadminng.lambda.Reducer;
import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
@@ -26,6 +27,8 @@ import java.util.stream.Collectors;
import static java.util.Arrays.asList;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.DROP_COMMENTS;
import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.DROP_COMMENTS;
import static org.assertj.core.api.Assertions.assertThat;
public abstract class ScenarioTest extends ContextBasedTest {
@@ -38,11 +41,11 @@ public abstract class ScenarioTest extends ContextBasedTest {
public String toString() {
return ObjectUtils.toString(uuid);
}
}
private final static Map<String, Alias<?>> aliases = new HashMap<>();
private final static Map<String, Object> properties = new HashMap<>();
private final static Map<String, Object> properties = new HashMap<>();
public final TestReport testReport = new TestReport(aliases);
@LocalServerPort
@@ -139,9 +142,9 @@ public abstract class ScenarioTest extends ContextBasedTest {
}
static UUID uuid(final String nameWithPlaceholders) {
final var resoledName = resolve(nameWithPlaceholders);
final UUID alias = ofNullable(knowVariables().get(resoledName)).filter(v -> v instanceof UUID).map(UUID.class::cast).orElse(null);
assertThat(alias).as("alias '" + resoledName + "' not found in aliases nor in properties [" +
final var resolvedName = resolve(nameWithPlaceholders, DROP_COMMENTS);
final UUID alias = ofNullable(knowVariables().get(resolvedName)).filter(v -> v instanceof UUID).map(UUID.class::cast).orElse(null);
assertThat(alias).as("alias '" + resolvedName + "' not found in aliases nor in properties [" +
knowVariables().keySet().stream().map(v -> "'" + v + "'").collect(Collectors.joining(", ")) + "]"
).isNotNull();
return alias;
@@ -162,13 +165,13 @@ public abstract class ScenarioTest extends ContextBasedTest {
return map;
}
public static String resolve(final String text) {
final var resolved = new TemplateResolver(text, ScenarioTest.knowVariables()).resolve();
public static String resolve(final String text, final Resolver resolver) {
final var resolved = new TemplateResolver(text, ScenarioTest.knowVariables()).resolve(resolver);
return resolved;
}
public static Object resolveTyped(final String text) {
final var resolved = resolve(text);
final var resolved = resolve(text, DROP_COMMENTS);
try {
return UUID.fromString(resolved);
} catch (final IllegalArgumentException e) {

View File

@@ -10,29 +10,39 @@ import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.DROP_COMMENTS;
public class TemplateResolver {
private final static Pattern pattern = Pattern.compile(",(\\s*})", Pattern.MULTILINE);
private static final String IF_NOT_FOUND_SYMBOL = "???";
public enum Resolver {
DROP_COMMENTS, // deletes comments ('#{whatever}' -> '')
KEEP_COMMENTS // keep comments ('#{whatever}' -> 'whatever')
}
enum PlaceholderPrefix {
RAW('%') {
@Override
String convert(final Object value) {
String convert(final Object value, final Resolver resolver) {
return value != null ? value.toString() : "";
}
},
JSON_QUOTED('$'){
@Override
String convert(final Object value) {
String convert(final Object value, final Resolver resolver) {
return jsonQuoted(value);
}
},
URI_ENCODED('&'){
@Override
String convert(final Object value) {
String convert(final Object value, final Resolver resolver) {
return value != null ? URLEncoder.encode(value.toString(), StandardCharsets.UTF_8) : "";
}
},
COMMENT('#'){
@Override
String convert(final Object value, final Resolver resolver) {
return resolver == DROP_COMMENTS ? "" : value.toString();
}
};
private final char prefixChar;
@@ -42,19 +52,24 @@ public class TemplateResolver {
}
static boolean contains(final char givenChar) {
return Arrays.stream(values()).anyMatch(p -> p.prefixChar == givenChar);
return Arrays.stream(values()).anyMatch(p -> p.prefixChar == givenChar);
}
static PlaceholderPrefix ofPrefixChar(final char givenChar) {
return Arrays.stream(values()).filter(p -> p.prefixChar == givenChar).findFirst().orElseThrow();
}
abstract String convert(final Object value);
abstract String convert(final Object value, final Resolver resolver);
}
private static final Pattern COMMA_RIGHT_BEFORE_CLOSING_BRACE = Pattern.compile(",(\\s*})", Pattern.MULTILINE);
private static final String IF_NOT_FOUND_SYMBOL = "???";
private final String template;
private final Map<String, Object> properties;
private final StringBuilder resolved = new StringBuilder();
private Resolver resolver;
private int position = 0;
public TemplateResolver(final String template, final Map<String, Object> properties) {
@@ -62,7 +77,8 @@ public class TemplateResolver {
this.properties = properties;
}
String resolve() {
String resolve(final Resolver resolver) {
this.resolver = resolver;
final var resolved = copy();
final var withoutDroppedLines = dropLinesWithNullProperties(resolved);
final var result = removeDanglingCommas(withoutDroppedLines);
@@ -70,7 +86,7 @@ public class TemplateResolver {
}
private static String removeDanglingCommas(final String withoutDroppedLines) {
return pattern.matcher(withoutDroppedLines).replaceAll("$1");
return COMMA_RIGHT_BEFORE_CLOSING_BRACE.matcher(withoutDroppedLines).replaceAll("$1");
}
private String dropLinesWithNullProperties(final String text) {
@@ -119,10 +135,10 @@ public class TemplateResolver {
placeholder.append(fetchChar());
}
}
final var name = new TemplateResolver(placeholder.toString(), properties).resolve();
final var value = propVal(name);
final var content = new TemplateResolver(placeholder.toString(), properties).resolve(resolver);
final var value = intro != '#' ? propVal(content) : content;
resolved.append(
PlaceholderPrefix.ofPrefixChar(intro).convert(value)
PlaceholderPrefix.ofPrefixChar(intro).convert(value, resolver)
);
skipChar('}');
}
@@ -134,12 +150,12 @@ public class TemplateResolver {
} else if (nameExpression.contains(IF_NOT_FOUND_SYMBOL)) {
final var parts = StringUtils.split(nameExpression, IF_NOT_FOUND_SYMBOL);
return Arrays.stream(parts).filter(Objects::nonNull).findFirst().orElseGet(() -> {
if ( parts[parts.length-1].isEmpty() ) {
// => whole expression ends with IF_NOT_FOUND_SYMBOL, thus last null element was optional
return null;
}
// => last alternative element in expression was null and not optional
throw new IllegalStateException("Missing required value in property-chain: " + nameExpression);
if ( parts[parts.length-1].isEmpty() ) {
// => whole expression ends with IF_NOT_FOUND_SYMBOL, thus last null element was optional
return null;
}
// => last alternative element in expression was null and not optional
throw new IllegalStateException("Missing required value in property-chain: " + nameExpression);
});
} else {
final var val = properties.get(nameExpression);

View File

@@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test;
import java.util.Map;
import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.DROP_COMMENTS;
import static org.assertj.core.api.Assertions.assertThat;
class TemplateResolverUnitTest {
@@ -42,7 +43,7 @@ class TemplateResolverUnitTest {
Map.entry("simple placeholder", "einfach"),
Map.entry("nested placeholder", "verschachtelt"),
Map.entry("with-special-chars", "3&3 AG")
)).resolve();
)).resolve(DROP_COMMENTS);
assertThat(resolved).isEqualTo("""
with optional JSON quotes:

View File

@@ -1,6 +1,8 @@
package net.hostsharing.hsadminng.hs.office.scenarios;
import lombok.SneakyThrows;
import net.hostsharing.hsadminng.system.SystemProcess;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.TestInfo;
@@ -9,29 +11,41 @@ import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
public class TestReport {
private final Map<String, ?> aliases;
private final StringBuilder markdownLog = new StringBuilder(); // records everything for debugging purposes
public static final File BUILD_DOC_SCENARIOS = new File("build/doc/scenarios");
private final static File markdownLogFile = new File(BUILD_DOC_SCENARIOS, ".last-debug-log.md");
public static final SimpleDateFormat MM_DD_YYYY_HH_MM_SS = new SimpleDateFormat("MM-dd-yyyy hh:mm:ss");
private PrintWriter markdownReport;
private final Map<String, ?> aliases;
private final PrintWriter markdownLog; // records everything for debugging purposes
private File markdownReportFile;
private PrintWriter markdownReport; // records only the use-case under test, without its pre-requisites
private int silent; // do not print anything to test-report if >0
static {
assertThat(BUILD_DOC_SCENARIOS.isDirectory() || BUILD_DOC_SCENARIOS.mkdirs())
.as("mkdir " + BUILD_DOC_SCENARIOS).isTrue();
}
@SneakyThrows
public TestReport(final Map<String, ?> aliases) {
this.aliases = aliases;
this.markdownLog = new PrintWriter(new FileWriter(markdownLogFile));
}
public void createTestLogMarkdownFile(final TestInfo testInfo) throws IOException {
final var testMethodName = testInfo.getTestMethod().map(Method::getName).orElseThrow();
final var testMethodOrder = testInfo.getTestMethod().map(m -> m.getAnnotation(Order.class).value()).orElseThrow();
assertThat(new File("doc/scenarios/").isDirectory() || new File("doc/scenarios/").mkdirs()).as("mkdir doc/scenarios/").isTrue();
markdownReport = new PrintWriter(new FileWriter("doc/scenarios/" + testMethodOrder + "-" + testMethodName + ".md"));
print("## Scenario #" + testInfo.getTestMethod().map(TestReport::orderNumber).orElseThrow() + ": " +
testMethodName.replaceAll("([a-z])([A-Z]+)", "$1 $2"));
markdownReportFile = new File(BUILD_DOC_SCENARIOS, testMethodOrder + "-" + testMethodName + ".md");
markdownReport = new PrintWriter(new FileWriter(markdownReportFile));
print("## Scenario #" + determineScenarioTitle(testInfo));
}
@SneakyThrows
@@ -45,7 +59,7 @@ public class TestReport {
}
// but the debugLog should contain all output, even if silent
markdownLog.append(outputWithCommentsForUuids);
markdownLog.print(outputWithCommentsForUuids);
}
public void printLine(final String output) {
@@ -56,10 +70,32 @@ public class TestReport {
printLine("\n" +output + "\n");
}
void silent(final Runnable code) {
silent++;
code.run();
silent--;
}
public void close() {
if (markdownReport != null) {
printPara("---");
printPara("generated on " + MM_DD_YYYY_HH_MM_SS.format(new Date()) + " for branch " + currentGitBranch());
markdownReport.close();
System.out.println("SCENARIO REPORT: " + asClickableLink(markdownReportFile));
}
markdownLog.close();
System.out.println("DEBUG LOG: " + asClickableLink(markdownLogFile));
}
private static @NotNull String determineScenarioTitle(final TestInfo testInfo) {
final var convertedTestMethodName =
testInfo.getTestMethod().map(TestReport::orderNumber).orElseThrow() + ": " +
testInfo.getTestMethod().map(Method::getName).map(t -> t.replaceAll("([a-z])([A-Z]+)", "$1 $2")).orElseThrow();
return convertedTestMethodName.replaceAll(": should ", ": ");
}
private String asClickableLink(final File file) {
return file.toURI().toString().replace("file:/", "file:///");
}
private static Object orderNumber(final Method method) {
@@ -83,10 +119,16 @@ public class TestReport {
return result.toString();
}
void silent(final Runnable code) {
silent++;
code.run();
silent--;
@SneakyThrows
private String currentGitBranch() {
try {
final var gitRevParse = new SystemProcess("git", "rev-parse", "--abbrev-ref", "HEAD");
gitRevParse.execute();
return gitRevParse.getStdOut().split("\\R", 2)[0];
} catch (final IOException exc) {
// TODO.test: the git call does not work in Jenkins, we have to find out why
System.err.println(exc);
return "unknown";
}
}
}

View File

@@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.office.scenarios;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.PathNotFoundException;
import io.restassured.http.ContentType;
import lombok.Getter;
import lombok.SneakyThrows;
@@ -33,6 +34,8 @@ import java.util.function.Function;
import java.util.function.Supplier;
import static java.net.URLEncoder.encode;
import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.DROP_COMMENTS;
import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.KEEP_COMMENTS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.platform.commons.util.StringUtils.isBlank;
@@ -50,6 +53,7 @@ public abstract class UseCase<T extends UseCase<?>> {
private final Map<String, Object> givenProperties = new LinkedHashMap<>();
private String nextTitle; // just temporary to override resultAlias for sub-use-cases
private String introduction;
public UseCase(final ScenarioTest testSuite) {
this(testSuite, getResultAliasFromProducesAnnotationInCallStack());
@@ -71,6 +75,9 @@ public abstract class UseCase<T extends UseCase<?>> {
}
public final HttpResponse doRun() {
if (introduction != null) {
testReport.printPara(introduction);
}
testReport.printPara("### Given Properties");
testReport.printLine("""
| name | value |
@@ -81,7 +88,7 @@ public abstract class UseCase<T extends UseCase<?>> {
testReport.silent(() ->
requirements.forEach((alias, factory) -> {
if (!ScenarioTest.containsAlias(alias)) {
factory.apply(alias).run().keep();
factory.apply(alias).run().keepAs(alias);
}
})
);
@@ -95,6 +102,11 @@ public abstract class UseCase<T extends UseCase<?>> {
protected void verify(final HttpResponse response) {
}
public UseCase<T> introduction(final String introduction) {
this.introduction = introduction;
return this;
}
public final UseCase<T> given(final String propName, final Object propValue) {
givenProperties.put(propName, propValue);
ScenarioTest.putProperty(propName, propValue);
@@ -106,11 +118,11 @@ public abstract class UseCase<T extends UseCase<?>> {
}
public final void obtain(
final String alias,
final String title,
final Supplier<HttpResponse> http,
final Function<HttpResponse, String> extractor,
final String... extraInfo) {
withTitle(ScenarioTest.resolve(alias), () -> {
withTitle(title, () -> {
final var response = http.get().keep(extractor);
Arrays.stream(extraInfo).forEach(testReport::printPara);
return response;
@@ -118,15 +130,15 @@ public abstract class UseCase<T extends UseCase<?>> {
}
public final void obtain(final String alias, final Supplier<HttpResponse> http, final String... extraInfo) {
withTitle(ScenarioTest.resolve(alias), () -> {
withTitle(alias, () -> {
final var response = http.get().keep();
Arrays.stream(extraInfo).forEach(testReport::printPara);
return response;
});
}
public HttpResponse withTitle(final String title, final Supplier<HttpResponse> code) {
this.nextTitle = title;
public HttpResponse withTitle(final String resolvableTitle, final Supplier<HttpResponse> code) {
this.nextTitle = resolvableTitle;
final var response = code.get();
this.nextTitle = null;
return response;
@@ -134,7 +146,7 @@ public abstract class UseCase<T extends UseCase<?>> {
@SneakyThrows
public final HttpResponse httpGet(final String uriPathWithPlaceholders) {
final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders);
final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders, DROP_COMMENTS);
final var request = HttpRequest.newBuilder()
.GET()
.uri(new URI("http://localhost:" + testSuite.port + uriPath))
@@ -147,7 +159,7 @@ public abstract class UseCase<T extends UseCase<?>> {
@SneakyThrows
public final HttpResponse httpPost(final String uriPathWithPlaceholders, final JsonTemplate bodyJsonTemplate) {
final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders);
final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders, DROP_COMMENTS);
final var requestBody = bodyJsonTemplate.resolvePlaceholders();
final var request = HttpRequest.newBuilder()
.POST(BodyPublishers.ofString(requestBody))
@@ -162,7 +174,7 @@ public abstract class UseCase<T extends UseCase<?>> {
@SneakyThrows
public final HttpResponse httpPatch(final String uriPathWithPlaceholders, final JsonTemplate bodyJsonTemplate) {
final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders);
final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders, DROP_COMMENTS);
final var requestBody = bodyJsonTemplate.resolvePlaceholders();
final var request = HttpRequest.newBuilder()
.method(HttpMethod.PATCH.toString(), BodyPublishers.ofString(requestBody))
@@ -177,7 +189,7 @@ public abstract class UseCase<T extends UseCase<?>> {
@SneakyThrows
public final HttpResponse httpDelete(final String uriPathWithPlaceholders) {
final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders);
final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders, DROP_COMMENTS);
final var request = HttpRequest.newBuilder()
.DELETE()
.uri(new URI("http://localhost:" + testSuite.port + uriPath))
@@ -197,7 +209,7 @@ public abstract class UseCase<T extends UseCase<?>> {
final String title,
final Supplier<UseCase.HttpResponse> http,
final Consumer<UseCase.HttpResponse>... assertions) {
withTitle(ScenarioTest.resolve(title), () -> {
withTitle(title, () -> {
final var response = http.get();
Arrays.stream(assertions).forEach(assertion -> assertion.accept(response));
return response;
@@ -209,7 +221,7 @@ public abstract class UseCase<T extends UseCase<?>> {
}
public String uriEncoded(final String text) {
return encode(ScenarioTest.resolve(text), StandardCharsets.UTF_8);
return encode(ScenarioTest.resolve(text, DROP_COMMENTS), StandardCharsets.UTF_8);
}
public static class JsonTemplate {
@@ -221,7 +233,7 @@ public abstract class UseCase<T extends UseCase<?>> {
}
String resolvePlaceholders() {
return ScenarioTest.resolve(template);
return ScenarioTest.resolve(template, DROP_COMMENTS);
}
}
@@ -266,7 +278,7 @@ public abstract class UseCase<T extends UseCase<?>> {
}
public HttpResponse keep(final Function<HttpResponse, String> extractor) {
final var alias = nextTitle != null ? nextTitle : resultAlias;
final var alias = nextTitle != null ? ScenarioTest.resolve(nextTitle, DROP_COMMENTS) : resultAlias;
assertThat(alias).as("cannot keep result, no alias found").isNotNull();
final var value = extractor.apply(this);
@@ -276,15 +288,20 @@ public abstract class UseCase<T extends UseCase<?>> {
return this;
}
public HttpResponse keep() {
final var alias = nextTitle != null ? nextTitle : resultAlias;
assertThat(alias).as("cannot keep result, no alias found").isNotNull();
public HttpResponse keepAs(final String alias) {
ScenarioTest.putAlias(
alias,
nonNullAlias(alias),
new ScenarioTest.Alias<>(UseCase.this.getClass(), locationUuid));
return this;
}
public HttpResponse keep() {
final var alias = nextTitle != null ? ScenarioTest.resolve(nextTitle, DROP_COMMENTS) : resultAlias;
assertThat(alias).as("cannot keep result, no title or alias found for locationUuid: " + locationUuid).isNotNull();
return keepAs(alias);
}
@SneakyThrows
public HttpResponse expectArrayElements(final int expectedElementCount) {
final var rootNode = objectMapper.readTree(response.body());
@@ -298,20 +315,20 @@ public abstract class UseCase<T extends UseCase<?>> {
@SneakyThrows
public String getFromBody(final String path) {
return JsonPath.parse(response.body()).read(ScenarioTest.resolve(path));
return JsonPath.parse(response.body()).read(ScenarioTest.resolve(path, DROP_COMMENTS));
}
@SneakyThrows
public Optional<String> getFromBodyAsOptional(final String path) {
public <T> Optional<T> getFromBodyAsOptional(final String path) {
try {
return Optional.ofNullable(JsonPath.parse(response.body()).read(ScenarioTest.resolve(path)));
} catch (final Exception e) {
return Optional.ofNullable(JsonPath.parse(response.body()).read(ScenarioTest.resolve(path, DROP_COMMENTS)));
} catch (final PathNotFoundException e) {
return null; // means the property did not exist at all, not that it was there with value null
}
}
@SneakyThrows
public OptionalAssert<String> path(final String path) {
public <T> OptionalAssert<T> path(final String path) {
return assertThat(getFromBodyAsOptional(path));
}
@@ -320,9 +337,9 @@ public abstract class UseCase<T extends UseCase<?>> {
// the title
if (nextTitle != null) {
testReport.printLine("\n### " + nextTitle + "\n");
testReport.printLine("\n### " + ScenarioTest.resolve(nextTitle, KEEP_COMMENTS) + "\n");
} else if (resultAlias != null) {
testReport.printLine("\n### " + resultAlias + "\n");
testReport.printLine("\n### Create " + resultAlias + "\n");
} else {
fail("please wrap the http...-call in the UseCase using `withTitle(...)`");
}
@@ -342,6 +359,13 @@ public abstract class UseCase<T extends UseCase<?>> {
testReport.printLine("```");
testReport.printLine("");
}
private String nonNullAlias(final String alias) {
// This marker tag should not appear in the source-code, as here is nothing to fix.
// But if it appears in generated Markdown files, it should show up when that marker tag is searched.
final var onlyVisibleInGeneratedMarkdownNotInSource = new String(new char[]{'F', 'I', 'X', 'M', 'E'});
return alias == null ? "unknown alias -- " + onlyVisibleInGeneratedMarkdownNotInSource : alias;
}
}
protected T self() {

View File

@@ -1,8 +1,8 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership;
import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON;
@@ -16,10 +16,18 @@ public class CreateMembership extends UseCase<CreateMembership> {
@Override
protected HttpResponse run() {
obtain("Partner: %{partnerName}", () ->
httpGet("/api/hs/office/partners?name=&{partnerName}")
.expecting(OK).expecting(JSON),
response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
"In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one."
);
return httpPost("/api/hs/office/memberships", usingJsonBody("""
{
"partner.uuid": ${Partner: Test AG},
"memberNumberSuffix": ${memberNumberSuffix},
"partner.uuid": ${Partner: %{partnerName}},
"memberNumberSuffix": ${%{memberNumberSuffix???}???00},
"status": "ACTIVE",
"validFrom": ${validFrom},
"membershipFeeBillable": ${membershipFeeBillable}

View File

@@ -0,0 +1,12 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
public class CreateCoopAssetsDepositTransaction extends CreateCoopAssetsTransaction {
public CreateCoopAssetsDepositTransaction(final ScenarioTest testSuite) {
super(testSuite);
given("transactionType", "DEPOSIT");
}
}

View File

@@ -0,0 +1,17 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
public class CreateCoopAssetsDisbursalTransaction extends CreateCoopAssetsTransaction {
public CreateCoopAssetsDisbursalTransaction(final ScenarioTest testSuite) {
super(testSuite);
}
@Override
protected HttpResponse run() {
given("transactionType", "DISBURSAL");
given("assetValue", "-%{valueToDisburse}");
return super.run();
}
}

View File

@@ -0,0 +1,27 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
public class CreateCoopAssetsRevertTransaction extends CreateCoopAssetsTransaction {
public CreateCoopAssetsRevertTransaction(final ScenarioTest testSuite) {
super(testSuite);
requires("CoopAssets-Transaction with incorrect assetValue", alias ->
new CreateCoopAssetsDepositTransaction(testSuite)
.given("memberNumber", "3101000")
.given("reference", "sign %{dateOfIncorrectTransaction}") // same as revertedAssetTx
.given("assetValue", 10)
.given("comment", "coop-assets deposit transaction with wrong asset value")
.given("transactionDate", "%{dateOfIncorrectTransaction}")
);
}
@Override
protected HttpResponse run() {
given("transactionType", "REVERSAL");
given("assetValue", -100);
given("revertedAssetTx", uuid("CoopAssets-Transaction with incorrect assetValue"));
return super.run();
}
}

View File

@@ -0,0 +1,53 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets;
import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON;
import static org.springframework.http.HttpStatus.OK;
public abstract class CreateCoopAssetsTransaction extends UseCase<CreateCoopAssetsTransaction> {
public CreateCoopAssetsTransaction(final ScenarioTest testSuite) {
super(testSuite);
}
@Override
protected HttpResponse run() {
obtain("#{Find }membershipUuid", () ->
httpGet("/api/hs/office/memberships?memberNumber=&{memberNumber}")
.expecting(OK).expecting(JSON).expectArrayElements(1),
response -> response.getFromBody("$[0].uuid")
);
return withTitle("Create the Coop-Assets-%{transactionType} Transaction", () ->
httpPost("/api/hs/office/coopassetstransactions", usingJsonBody("""
{
"membership.uuid": ${membershipUuid},
"transactionType": ${transactionType},
"reference": ${reference},
"assetValue": ${assetValue},
"comment": ${comment},
"valueDate": ${transactionDate},
"revertedAssetTx.uuid": ${revertedAssetTx???}
}
"""))
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
);
}
@Override
protected void verify(final HttpResponse response) {
verify("Verify Coop-Assets %{transactionType}-Transaction",
() -> httpGet("/api/hs/office/coopassetstransactions/" + response.getLocationUuid())
.expecting(HttpStatus.OK).expecting(ContentType.JSON),
path("transactionType").contains("%{transactionType}"),
path("assetValue").contains("%{assetValue}"),
path("comment").contains("%{comment}"),
path("valueDate").contains("%{transactionDate}")
);
}
}

View File

@@ -0,0 +1,17 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
public class CreateCoopSharesCancellationTransaction extends CreateCoopSharesTransaction {
public CreateCoopSharesCancellationTransaction(final ScenarioTest testSuite) {
super(testSuite);
}
@Override
protected HttpResponse run() {
given("transactionType", "CANCELLATION");
given("shareCount", "-%{sharesToCancel}");
return super.run();
}
}

View File

@@ -0,0 +1,27 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
public class CreateCoopSharesRevertTransaction extends CreateCoopSharesTransaction {
public CreateCoopSharesRevertTransaction(final ScenarioTest testSuite) {
super(testSuite);
requires("CoopShares-Transaction with incorrect shareCount", alias ->
new CreateCoopSharesSubscriptionTransaction(testSuite)
.given("memberNumber", "3101000")
.given("reference", "sign %{dateOfIncorrectTransaction}") // same as revertedShareTx
.given("shareCount", 100)
.given("comment", "coop-shares subscription transaction with wrong share count")
.given("transactionDate", "%{dateOfIncorrectTransaction}")
);
}
@Override
protected HttpResponse run() {
given("transactionType", "REVERSAL");
given("shareCount", -100);
given("revertedShareTx", uuid("CoopShares-Transaction with incorrect shareCount"));
return super.run();
}
}

View File

@@ -0,0 +1,12 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
public class CreateCoopSharesSubscriptionTransaction extends CreateCoopSharesTransaction {
public CreateCoopSharesSubscriptionTransaction(final ScenarioTest testSuite) {
super(testSuite);
given("transactionType", "SUBSCRIPTION");
}
}

View File

@@ -0,0 +1,53 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares;
import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON;
import static org.springframework.http.HttpStatus.OK;
public abstract class CreateCoopSharesTransaction extends UseCase<CreateCoopSharesTransaction> {
public CreateCoopSharesTransaction(final ScenarioTest testSuite) {
super(testSuite);
}
@Override
protected HttpResponse run() {
obtain("#{Find }membershipUuid", () ->
httpGet("/api/hs/office/memberships?memberNumber=&{memberNumber}")
.expecting(OK).expecting(JSON).expectArrayElements(1),
response -> response.getFromBody("$[0].uuid")
);
return withTitle("Create the Coop-Shares-%{transactionType} Transaction", () ->
httpPost("/api/hs/office/coopsharestransactions", usingJsonBody("""
{
"membership.uuid": ${membershipUuid},
"transactionType": ${transactionType},
"reference": ${reference},
"shareCount": ${shareCount},
"comment": ${comment},
"valueDate": ${transactionDate},
"revertedShareTx.uuid": ${revertedShareTx???}
}
"""))
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
);
}
@Override
protected void verify(final HttpResponse response) {
verify("Verify Coop-Shares %{transactionType}-Transaction",
() -> httpGet("/api/hs/office/coopsharestransactions/" + response.getLocationUuid())
.expecting(HttpStatus.OK).expecting(ContentType.JSON),
path("transactionType").contains("%{transactionType}"),
path("shareCount").contains("%{shareCount}"),
path("comment").contains("%{comment}"),
path("valueDate").contains("%{transactionDate}")
);
}
}

View File

@@ -16,6 +16,8 @@ public class CreatePartner extends UseCase<CreatePartner> {
public CreatePartner(final ScenarioTest testSuite) {
super(testSuite);
introduction("A partner can be a client or a vendor, currently we only use them for clients.");
}
@Override