From 9b7c2c4bc4b7b04c3bebe7376f05bf3761a33bab Mon Sep 17 00:00:00 2001 From: Michal Landsman Date: Thu, 12 Nov 2026 20:47:01 +0100 Subject: [PATCH 1/5] be: assert the czech text moneta sends, so a body decoded as anything but utf-8 fails the build --- .../test/java/cz/insuit/moneta/MonetaClientTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/modules/moneta-api/src/test/java/cz/insuit/moneta/MonetaClientTest.java b/modules/moneta-api/src/test/java/cz/insuit/moneta/MonetaClientTest.java index e855bc5..4829622 100644 --- a/modules/moneta-api/src/test/java/cz/insuit/moneta/MonetaClientTest.java +++ b/modules/moneta-api/src/test/java/cz/insuit/moneta/MonetaClientTest.java @@ -264,6 +264,17 @@ void aPendingMovementHasOnlyTheDateItWasEntered() { assertNull(pending.bookingDate()); assertNull(pending.valueDate()); assertEquals(new PaymentSymbols(null, null, null), pending.symbols()); + // Czech text arrives intact only while the body is decoded as UTF-8. + assertEquals("Okamžitá platba", pending.message()); + } + + @Test + void aBookedMovementKeepsItsCzechText() { + answer(TRANSACTIONS, 200, fixture("live/booked.json")); + + var booked = client.transactions(TOKEN, "acc-1", FROM, TO).getFirst(); + + assertTrue(booked.raw().contains("OKAMŽITÁ ÚHRADA"), booked.raw()); } @Test From 6a8931d00daa57ea8f81df18ebd217c236703494 Mon Sep 17 00:00:00 2001 From: Michal Landsman Date: Thu, 12 Nov 2026 20:47:01 +0100 Subject: [PATCH 2/5] be: call each of the holder's delegates, so one reading the wrong account fails a test --- .../cz/insuit/moneta/MonetaClientTest.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/modules/moneta-api/src/test/java/cz/insuit/moneta/MonetaClientTest.java b/modules/moneta-api/src/test/java/cz/insuit/moneta/MonetaClientTest.java index 4829622..5fe4ee5 100644 --- a/modules/moneta-api/src/test/java/cz/insuit/moneta/MonetaClientTest.java +++ b/modules/moneta-api/src/test/java/cz/insuit/moneta/MonetaClientTest.java @@ -616,6 +616,33 @@ void aHolderReadsTheAccountsAgainForEveryIban() { .count()); } + @Test + void aHolderReadsTheAccountItNames() { + answer(ACCOUNTS, 200, TWO_ACCOUNTS_ONE_WITHOUT_IBAN); + answer(ACCOUNTS + "/acc-0/balance", 200, fixture("live/balance.json")); + answer(ACCOUNTS + "/acc-0/transactions", 200, NO_TRANSACTIONS); + var me = client.with(TOKEN); + + assertEquals( + List.of("acc-0", "acc-1"), + me.accounts().stream().map(Account::id).toList()); + me.balance("acc-0"); + me.transactions("acc-0").between(FROM, TO).list(); + + assertEquals( + List.of(ACCOUNTS, ACCOUNTS + "/acc-0/balance", ACCOUNTS + "/acc-0/transactions"), + requests.stream().map(r -> r.getRequestURI().getPath()).toList()); + } + + @Test + void aHolderBalanceWithoutAnAccountRefusesATokenWithTwo() { + answer(ACCOUNTS, 200, TWO_ACCOUNTS_ONE_WITHOUT_IBAN); + + var refused = assertThrows( + IllegalStateException.class, () -> client.with(TOKEN).balance()); + assertTrue(String.valueOf(refused.getMessage()).contains("opens 2 accounts"), refused.getMessage()); + } + @Test void aQueryWithoutAnAccountRefusesATokenWithTwo() { answer(ACCOUNTS, 200, """ From a1ad1df819a52ab580cc43d44c35d6b72ed7ac5d Mon Sep 17 00:00:00 2001 From: Michal Landsman Date: Thu, 12 Nov 2026 20:47:01 +0100 Subject: [PATCH 3/5] be: test that an unknown way of paying is other and that a cancellation is reversed --- .../cz/insuit/moneta/MonetaClientTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/modules/moneta-api/src/test/java/cz/insuit/moneta/MonetaClientTest.java b/modules/moneta-api/src/test/java/cz/insuit/moneta/MonetaClientTest.java index 5fe4ee5..783976f 100644 --- a/modules/moneta-api/src/test/java/cz/insuit/moneta/MonetaClientTest.java +++ b/modules/moneta-api/src/test/java/cz/insuit/moneta/MonetaClientTest.java @@ -277,6 +277,29 @@ void aBookedMovementKeepsItsCzechText() { assertTrue(booked.raw().contains("OKAMŽITÁ ÚHRADA"), booked.raw()); } + @Test + void anUnknownWayOfPayingIsOtherAndKeptInRaw() { + var page = (ObjectNode) JSON.readTree(movement("BOOK", "CRDT")); + ((ObjectNode) page.get("transactions").get(0)) + .withObject("/entryDetails/transactionDetails/references") + .put("clearingSystemReference", "PAYMENT_BY_CARRIER_PIGEON"); + answer(TRANSACTIONS, 200, page.toString()); + + var movement = client.transactions(TOKEN, "acc-1", FROM, TO).getFirst(); + + assertEquals(Transaction.Method.OTHER, movement.method()); + assertTrue(movement.raw().contains("PAYMENT_BY_CARRIER_PIGEON"), movement.raw()); + } + + @Test + void aCancellationIsReversed() { + var page = (ObjectNode) JSON.readTree(movement("BOOK", "DBIT")); + ((ObjectNode) page.get("transactions").get(0)).put("reversalIndicator", true); + answer(TRANSACTIONS, 200, page.toString()); + + assertTrue(client.transactions(TOKEN, "acc-1", FROM, TO).getFirst().reversed()); + } + @Test void readsSymbolsAndSignsAnOutgoingPaymentAsMonetaSendsThem() { // Recorded from the live API: a DBIT comes with a positive value, the symbols in one string. From 8d1cdff13c6bc393b03ca68c47c28fd69de925fe Mon Sep 17 00:00:00 2001 From: Michal Landsman Date: Thu, 12 Nov 2026 20:47:02 +0100 Subject: [PATCH 4/5] be: test money without minor digits, negation and a null iban, the edges the suite skipped --- .../java/cz/insuit/moneta/MonetaClientTest.java | 7 +++++++ .../src/test/java/cz/insuit/moneta/MoneyTest.java | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/modules/moneta-api/src/test/java/cz/insuit/moneta/MonetaClientTest.java b/modules/moneta-api/src/test/java/cz/insuit/moneta/MonetaClientTest.java index 783976f..8bfd471 100644 --- a/modules/moneta-api/src/test/java/cz/insuit/moneta/MonetaClientTest.java +++ b/modules/moneta-api/src/test/java/cz/insuit/moneta/MonetaClientTest.java @@ -585,6 +585,13 @@ void anySeparatorCopiedFromAStatementOrAPageMatches() { assertEquals("acc-1", Objects.requireNonNull(found).id()); } + @Test + @SuppressWarnings("NullAway") // a Java caller can still pass null + void aNullIbanIsRefusedBeforeAnyRequest() { + assertThrows(NullPointerException.class, () -> client.accountByIban(TOKEN, null)); + assertTrue(requests.isEmpty()); + } + @ParameterizedTest @ValueSource(strings = {"", " ", "\u00A0", "-"}) void aBlankIbanIsTheCallersMistakeNotARevokedAccount(String blank) { diff --git a/modules/moneta-api/src/test/java/cz/insuit/moneta/MoneyTest.java b/modules/moneta-api/src/test/java/cz/insuit/moneta/MoneyTest.java index 568c85e..baa3c1e 100644 --- a/modules/moneta-api/src/test/java/cz/insuit/moneta/MoneyTest.java +++ b/modules/moneta-api/src/test/java/cz/insuit/moneta/MoneyTest.java @@ -24,6 +24,19 @@ void neverRounds() { assertEquals(new BigDecimal("100"), new Money(new BigDecimal("100.0"), Currency.getInstance("JPY")).amount()); } + @Test + void negatesWithTheSameCurrency() { + assertEquals(new Money(new BigDecimal("-12.50"), CZK), new Money(new BigDecimal("12.5"), CZK).negate()); + } + + @Test + void aCurrencyWithoutMinorDigitsKeepsTheAmountAsWritten() { + // XXX has getDefaultFractionDigits() == -1, which would otherwise be a negative scale. + var none = Currency.getInstance("XXX"); + assertEquals(new BigDecimal("12.5"), new Money(new BigDecimal("12.50"), none).amount()); + assertEquals(new BigDecimal("100"), new Money(new BigDecimal("100"), none).amount()); + } + @Test void printsAsAnAmountAndACode() { assertEquals("1234.10 CZK", new Money(new BigDecimal("1234.1"), CZK).toString()); From 379848c8f59299691a2870c6dd98248869ea038e Mon Sep 17 00:00:00 2001 From: Michal Landsman Date: Thu, 12 Nov 2026 20:47:02 +0100 Subject: [PATCH 5/5] be: pin that kotlin sees accountByIban as nullable, so a dropped annotation fails the build --- .../test/kotlin/example/kotlin/KotlinCallerTest.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/testing/kotlin/src/test/kotlin/example/kotlin/KotlinCallerTest.kt b/testing/kotlin/src/test/kotlin/example/kotlin/KotlinCallerTest.kt index 8d5d53c..2aab9db 100644 --- a/testing/kotlin/src/test/kotlin/example/kotlin/KotlinCallerTest.kt +++ b/testing/kotlin/src/test/kotlin/example/kotlin/KotlinCallerTest.kt @@ -41,6 +41,18 @@ class KotlinCallerTest { assertTrue(errors.isNotEmpty(), "iban read as non-null compiled, so Kotlin sees it as a platform type") } + @Test + fun aLookupThatMayFindNothingIsHandledWithAnElvis() { + val source = "fun f(me: cz.insuit.moneta.AccountHolder): String = me.accountByIban(\"x\")?.id ?: \"none\"" + assertEquals(listOf(), compile(source)) + } + + @Test + fun aLookupThatMayFindNothingCannotBeReadAsNonNull() { + val errors = compile("fun f(me: cz.insuit.moneta.AccountHolder): String = me.accountByIban(\"x\").id") + assertTrue(errors.isNotEmpty(), "accountByIban read as non-null compiled, so its @Nullable is lost") + } + /** The errors compiling [source] against the library gives, empty when it compiles. */ private fun compile(source: String): List { val dir = createTempDirectory()