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..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 @@ -264,6 +264,40 @@ 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 + 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 @@ -551,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) { @@ -605,6 +646,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, """ 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()); 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()