libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

CoreBankApiTest.kt (93326B)


      1 /*
      2  * This file is part of LibEuFin.
      3  * Copyright (C) 2023-2025 Taler Systems S.A.
      4 
      5  * LibEuFin is free software; you can redistribute it and/or modify
      6  * it under the terms of the GNU Affero General Public License as
      7  * published by the Free Software Foundation; either version 3, or
      8  * (at your option) any later version.
      9 
     10  * LibEuFin is distributed in the hope that it will be useful, but
     11  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     12  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
     13  * Public License for more details.
     14 
     15  * You should have received a copy of the GNU Affero General Public
     16  * License along with LibEuFin; see the file COPYING.  If not, see
     17  * <http://www.gnu.org/licenses/>
     18  */
     19 
     20 import io.ktor.client.request.*
     21 import io.ktor.client.statement.*
     22 import io.ktor.http.*
     23 import io.ktor.server.testing.*
     24 import kotlinx.serialization.json.JsonElement
     25 import org.junit.Test
     26 import tech.libeufin.bank.*
     27 import tech.libeufin.bank.auth.TOKEN_PREFIX
     28 import tech.libeufin.common.*
     29 import tech.libeufin.common.crypto.CryptoUtil
     30 import tech.libeufin.common.db.*
     31 import tech.libeufin.common.test.*
     32 import java.time.Duration
     33 import java.time.Instant
     34 import java.util.*
     35 import kotlin.test.*
     36 
     37 class CoreBankSecurityTest {
     38     @Test
     39     fun passwordUpdate() = bankSetup { db ->
     40         suspend fun currentHash(): String {
     41             return db.serializable(
     42                 "SELECT password_hash FROM customers WHERE username='customer'"
     43             ) {
     44                 one {
     45                     it.getString(1)
     46                 }
     47             }
     48         }
     49 
     50         // Set outdated hash
     51         val password = "customer-password"
     52         val pwh = CryptoUtil.hashStringSHA256(password).encodeBase64()
     53         val hash = "sha256\$$pwh"
     54         db.serializable(
     55             "UPDATE customers SET password_hash=? WHERE username='customer'"
     56         ) {
     57             bind(hash)
     58             executeUpdate()
     59         }
     60         assertEquals(hash, currentHash())
     61 
     62         // Check hash is updated
     63         client.getA("/accounts/customer").assertOk()
     64         val newHash = currentHash()
     65         assert(hash != newHash)
     66 
     67         // Check hash stay the same
     68         client.getA("/accounts/customer").assertOk()
     69         assertEquals(newHash, currentHash())
     70     }
     71 }
     72 
     73 class CoreBankConfigTest {
     74     // GET /config
     75     @Test
     76     fun config() = bankSetup { 
     77         client.get("/config").assertOk()
     78     }
     79 
     80     // GET /monitor
     81     @Test
     82     fun monitor() = bankSetup { 
     83         authRoutine(HttpMethod.Get, "/monitor", requireAdmin = true)
     84         // Check OK
     85         client.getAdmin("/monitor?timeframe=day&which=25").assertOk()
     86         client.getAdmin("/monitor?timeframe=day=which=25").assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED)
     87     }
     88 }
     89 
     90 class CoreBankTokenApiTest {
     91     // POST /accounts/USERNAME/token
     92     @Test
     93     fun post() = bankSetup { db -> 
     94         authRoutine(HttpMethod.Post, "/accounts/merchant/token")
     95 
     96         // Unknown account
     97         client.post("/accounts/merchant/token") {
     98             basicAuth("unknown", "password")
     99         }.assertUnauthorized()
    100 
    101         // Wrong password
    102         client.post("/accounts/merchant/token") {
    103             basicAuth("merchant", "wrong-password")
    104         }.assertUnauthorized()
    105 
    106         // Wrong account
    107         client.post("/accounts/merchant/token") {
    108             basicAuth("exchange", "merchant-password")
    109         }.assertUnauthorized()
    110 
    111         // New default token
    112         client.postPw("/accounts/merchant/token") {
    113             json { "scope" to "readonly" }
    114         }.assertOkJson<TokenSuccessResponse> {
    115             // Checking that the token lifetime defaulted to 24 hours.
    116             val token = db.token.access(Base32Crockford.decode(it.access_token.removePrefix(TOKEN_PREFIX)), Instant.now())
    117             val lifeTime = Duration.between(token!!.creationTime, token.expirationTime)
    118             assertEquals(Duration.ofDays(1), lifeTime)
    119         }
    120 
    121         // Check default duration
    122         client.postPw("/accounts/merchant/token") {
    123             json { "scope" to "readonly" }
    124         }.assertOkJson<TokenSuccessResponse> {
    125             // Checking that the token lifetime defaulted to 24 hours.
    126             val token = db.token.access(Base32Crockford.decode(it.access_token.removePrefix(TOKEN_PREFIX)), Instant.now())
    127             val lifeTime = Duration.between(token!!.creationTime, token.expirationTime)
    128             assertEquals(Duration.ofDays(1), lifeTime)
    129         }
    130 
    131         // Check valid refresh scope
    132         for ((fromScope, toScope) in listOf(
    133             "readwrite" to "readwrite",
    134             "readonly" to "readonly",
    135             "revenue" to "revenue",
    136             "readwrite" to "readonly",
    137             "readwrite" to "revenue",
    138             "readonly" to "revenue",
    139         )) {
    140             client.postPw("/accounts/merchant/token") {
    141                 json { 
    142                     "scope" to fromScope
    143                     "refreshable" to true
    144                 }
    145             }.assertOkJson<TokenSuccessResponse> {
    146                 val token = it.access_token
    147                 client.post("/accounts/merchant/token") {
    148                     headers[HttpHeaders.Authorization] = "Bearer $token"
    149                     json { "scope" to toScope }
    150                 }.assertOk()
    151             }
    152         }
    153 
    154         // Check invalid refresh scope
    155         for ((fromScope, toScope) in listOf(
    156             "readonly" to "readwrite",
    157             "revenue" to "readonly",
    158             "revenue" to "readwrite"
    159         )) {
    160             client.postPw("/accounts/merchant/token") {
    161                 json { 
    162                     "scope" to fromScope
    163                     "refreshable" to true
    164                 }
    165             }.assertOkJson<TokenSuccessResponse> {
    166                 val token = it.access_token
    167                 client.post("/accounts/merchant/token") {
    168                     headers[HttpHeaders.Authorization] = "Bearer $token"
    169                     json { "scope" to toScope }
    170                 }.assertForbidden(TalerErrorCode.GENERIC_TOKEN_PERMISSION_INSUFFICIENT)
    171             }
    172         }
    173 
    174         // Check no refreshable
    175         client.postPw("/accounts/merchant/token") {
    176             json { 
    177                 "scope" to "readonly"
    178             }
    179         }.assertOkJson<TokenSuccessResponse> {
    180             val token = it.access_token
    181             client.post("/accounts/merchant/token") {
    182                 headers[HttpHeaders.Authorization] = "Bearer $token"
    183                 json { "scope" to "readonly" }
    184             }.assertForbidden(TalerErrorCode.GENERIC_TOKEN_PERMISSION_INSUFFICIENT)
    185         }
    186         
    187         // Check 'forever' case.
    188         client.postPw("/accounts/merchant/token") {
    189             json { 
    190                 "scope" to "readonly"
    191                 "duration" to obj {
    192                     "d_us" to "forever"
    193                 }
    194             }
    195         }.assertOkJson<TokenSuccessResponse> {
    196             assertEquals(Instant.MAX, it.expiration.instant)
    197         }
    198 
    199         // Check too big or invalid durations
    200         client.postPw("/accounts/merchant/token") {
    201             json { 
    202                 "scope" to "readonly"
    203                 "duration" to obj {
    204                     "d_us" to "invalid"
    205                 }
    206             }
    207         }.assertBadRequest()
    208         client.postPw("/accounts/merchant/token") {
    209             json { 
    210                 "scope" to "readonly"
    211                 "duration" to obj {
    212                     "d_us" to Long.MAX_VALUE
    213                 }
    214             }
    215         }.assertBadRequest()
    216         client.postPw("/accounts/merchant/token") {
    217             json { 
    218                 "scope" to "readonly"
    219                 "duration" to obj {
    220                     "d_us" to -1
    221                 }
    222             }
    223         }.assertBadRequest()
    224     }
    225 
    226     @Test
    227     fun post2FA() = bankSetup { db -> 
    228         // Setup a known phone 2FA
    229         client.patchA("/accounts/merchant") {
    230             json {
    231                 "contact_data" to obj {
    232                     "phone" to "+12345"
    233                 }
    234                 "tan_channel" to "sms"
    235             }
    236         }.assertChallenge().assertNoContent()
    237 
    238         // Check creating a token requires to solve an unauthenticated challenge
    239         val challenge = client.postPw("/accounts/merchant/token") {
    240             json { "scope" to "readonly" }
    241         }.assertAcceptedJson<ChallengeResponse>().challenges[0]
    242         client.post("/accounts/merchant/challenge/${challenge.challenge_id}")
    243             .assertOk()
    244         assertEquals("REDACTED", challenge.tan_info) // Check phone number is hidden
    245         val code = tanCode("+12345")
    246         client.post("/accounts/merchant/challenge/${challenge.challenge_id}/confirm") {
    247             json { "tan" to code }
    248         }.assertNoContent()
    249         client.postPw("/accounts/merchant/token") {
    250             headers[TALER_CHALLENGE_IDS] = "${challenge.challenge_id}"
    251             json { "scope" to "readonly" }
    252         }.assertOkJson<TokenSuccessResponse>()
    253     }
    254 
    255     @Test
    256     fun locked() = bankSetup { db -> 
    257         // Setup a known phone 2FA
    258         client.patchA("/accounts/merchant") {
    259             json {
    260                 "contact_data" to obj {
    261                     "phone" to "+12345"
    262                 }
    263                 "tan_channel" to "sms"
    264             }
    265         }.assertChallenge().assertNoContent()
    266 
    267         suspend fun blockAccount() {
    268             var counter = MAX_TOKEN_CREATION_ATTEMPTS + 1
    269             while (counter > 0) {
    270                 val challenge = client.postPw("/accounts/merchant/token") {
    271                     json { "scope" to "readonly" }
    272                 }.assertAcceptedJson<ChallengeResponse>().challenges[0]
    273                 client.post("/accounts/merchant/challenge/${challenge.challenge_id}")
    274                     .assertOk()
    275                 while (counter > 0) {
    276                     val error = client.post("/accounts/merchant/challenge/${challenge.challenge_id}/confirm"){
    277                         json { "tan" to "bad code" } 
    278                     }.json<TalerError>()
    279                     counter -= 1
    280                     when (error.code) {
    281                         TalerErrorCode.BANK_TAN_CHALLENGE_FAILED.code -> continue
    282                         TalerErrorCode.BANK_TAN_RATE_LIMITED.code, TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED.code -> break
    283                         else -> throw Exception("$error")
    284                     }
    285                 }
    286             }
    287             client.postPw("/accounts/merchant/token") {
    288                 json { "scope" to "readonly" }
    289             }.assertForbidden(TalerErrorCode.BANK_ACCOUNT_LOCKED)
    290         }
    291 
    292         blockAccount()
    293 
    294         // Check token still works
    295         client.getA("/accounts/merchant").assertOkJson<AccountData> {
    296             assertTrue(it.is_locked)
    297         }
    298 
    299         // Check admin can unlock
    300         client.patchAdmin("/accounts/merchant/auth") {
    301             json {
    302                 "new_password" to "merchant-password"
    303             }
    304         }.assertNoContent()
    305         client.getA("/accounts/merchant").assertOkJson<AccountData> {
    306             assertFalse(it.is_locked)
    307         }
    308         blockAccount()
    309 
    310         // Check token can unlock
    311         client.patchA("/accounts/merchant/auth") {
    312             json {
    313                 "old_password" to "merchant-password"
    314                 "new_password" to "merchant-password"
    315             }
    316         }.assertChallenge().assertNoContent()
    317         client.getA("/accounts/merchant").assertOkJson<AccountData> {
    318             assertFalse(it.is_locked)
    319         }
    320     }
    321 
    322     // DELETE /accounts/USERNAME/token
    323     @Test
    324     fun delete() = bankSetup { 
    325         val token = client.postPw("/accounts/merchant/token") {
    326             json { "scope" to "readonly" }
    327         }.assertOkJson<TokenSuccessResponse>().access_token
    328         // Check OK
    329         client.delete("/accounts/merchant/token") {
    330             headers[HttpHeaders.Authorization] = "Bearer $token"
    331         }.assertNoContent()
    332         // Check token no longer work
    333         client.delete("/accounts/merchant/token") {
    334             headers[HttpHeaders.Authorization] = "Bearer $token"
    335         }.assertUnauthorized(TalerErrorCode.GENERIC_TOKEN_UNKNOWN)
    336     }
    337 
    338     // DELETE /accounts/USERNAME/tokens/TOKEN_ID
    339     @Test
    340     fun deleteById() = bankSetup {
    341         authRoutine(HttpMethod.Delete, "/accounts/merchant/tokens/1t", allowAdmin = true)
    342         
    343         val token = client.postPw("/accounts/merchant/token") {
    344             json { "scope" to "readonly" }
    345         }.assertOkJson<TokenSuccessResponse>().access_token
    346         // Check OK
    347         client.deleteA("/accounts/merchant/tokens/2").assertNoContent()
    348         client.deleteA("/accounts/merchant/tokens/2").assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
    349         // Check token no longer work
    350         client.delete("/accounts/merchant/token") {
    351             headers[HttpHeaders.Authorization] = "Bearer $token"
    352         }.assertUnauthorized(TalerErrorCode.GENERIC_TOKEN_UNKNOWN)
    353     }
    354 
    355     // GET /accounts/USERNAME/tokens
    356     @Test
    357     fun get() = bankSetup {
    358         // Check OK
    359         for (account in listOf("merchant", "customer")) {
    360             client.getA("/accounts/$account/tokens").assertOkJson<TokenInfos> {
    361                 assertEquals(1, it.tokens.size)
    362             }
    363         }
    364         client.postPw("/accounts/merchant/token") {
    365             json { "scope" to "readonly" }
    366         }.assertOk()
    367         client.postPw("/accounts/merchant/token") {
    368             json { "scope" to "readwrite" }
    369         }.assertOk()
    370         client.postPw("/accounts/customer/token") {
    371             json {
    372                 "scope" to "revenue"
    373                 "description" to "description"
    374             }
    375         }.assertOk()
    376         client.getA("/accounts/merchant/tokens").assertOkJson<TokenInfos> {
    377             assertEquals(3, it.tokens.size)
    378             for (token in it.tokens) {
    379                 assertNull(token.description)
    380             }
    381         }
    382         client.getA("/accounts/customer/tokens").assertOkJson<TokenInfos> {
    383             assertEquals(2, it.tokens.size)
    384             assertEquals("description", it.tokens[0].description)
    385         }
    386     }
    387 }
    388 
    389 class CoreBankAccountsApiTest {
    390     // POST /accounts
    391     @Test
    392     fun create() = bankSetup { 
    393         // Check generated payto
    394         obj {
    395             "username" to "john"
    396             "password" to "password"
    397             "name" to "John"
    398         }.let { req ->
    399             // Check Ok
    400             val payto = client.post("/accounts") {
    401                 json(req)
    402             }.assertOkJson<RegisterAccountResponse>().internal_payto_uri
    403             // Check idempotency
    404             client.post("/accounts") {
    405                 json(req)
    406             }.assertOkJson<RegisterAccountResponse> {
    407                 assertEquals(payto, it.internal_payto_uri)
    408             }
    409             // Check idempotency with payto
    410             client.post("/accounts") {
    411                 json(req) {
    412                     "payto_uri" to payto
    413                 }
    414             }.assertOk()
    415             // Check payto conflict
    416             client.post("/accounts") {
    417                 json(req) {
    418                     "payto_uri" to IbanPayto.rand()
    419                 }
    420             }.assertConflict(TalerErrorCode.BANK_REGISTER_USERNAME_REUSE)
    421         }
    422 
    423         // Check given payto
    424         val payto = IbanPayto.rand()
    425         val req = obj {
    426             "username" to "foo"
    427             "password" to "password"
    428             "name" to "Jane"
    429             "is_public" to true
    430             "payto_uri" to payto
    431             "is_taler_exchange" to true
    432         }
    433         // Check Ok
    434         client.post("/accounts") {
    435             json(req)
    436         }.assertOkJson<RegisterAccountResponse> {
    437             assertEquals(payto.full("Jane"), it.internal_payto_uri)
    438         }
    439         // Testing idempotency
    440         client.post("/accounts") {
    441             json(req)
    442         }.assertOkJson<RegisterAccountResponse> {
    443             assertEquals(payto.full("Jane"), it.internal_payto_uri)
    444         }
    445         // Check admin only debit_threshold
    446         obj {
    447             "username" to "bat"
    448             "password" to "password"
    449             "name" to "Bat"
    450             "debit_threshold" to "KUDOS:42"
    451         }.let { req ->
    452             client.post("/accounts") {
    453                 json(req)
    454             }.assertConflict(TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT)
    455             client.postAdmin("/accounts") {
    456                 json(req)
    457             }.assertOk()
    458         }
    459 
    460         // Check admin only conversion_rate_class_id
    461         createConversionRateClass()
    462         obj {
    463             "username" to "bat2"
    464             "password" to "password"
    465             "name" to "Bat"
    466             "conversion_rate_class_id" to 1
    467         }.let { req ->
    468             client.post("/accounts") {
    469                 json(req)
    470             }.assertConflict(TalerErrorCode.BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS)
    471             client.postAdmin("/accounts") {
    472                 json(req)
    473             }.assertOk()
    474         }
    475 
    476         // Check admin only tan_channel
    477         obj {
    478             "username" to "bat3"
    479             "password" to "password"
    480             "name" to "Bat"
    481             "contact_data" to obj {
    482                 "phone" to "+456"
    483             }
    484             "tan_channel" to "sms"
    485         }.let { req ->
    486             client.post("/accounts") {
    487                 json(req)
    488             }.assertConflict(TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL)
    489             client.postAdmin("/accounts") {
    490                 json(req)
    491             }.assertOk()
    492         }
    493 
    494         // Check both tan channels
    495         client.postAdmin("/accounts") {
    496             json { 
    497                 "username" to "bat2"
    498                 "password" to "password"
    499                 "name" to "Bat"
    500                 "tan_channel" to "sms"
    501                 "tan_channels" to emptyList<String>()
    502             }
    503         }.assertBadRequest()
    504 
    505         // Check tan info
    506         val channels = listOf("sms", "email")
    507         for (channel in channels) {
    508             client.postAdmin("/accounts") {
    509                 json { 
    510                     "username" to "bat2"
    511                     "password" to "password"
    512                     "name" to "Bat"
    513                     "tan_channel" to channel
    514                 }
    515             }.assertConflict(TalerErrorCode.BANK_MISSING_TAN_INFO)
    516             client.postAdmin("/accounts") {
    517                 json { 
    518                     "username" to "bat2"
    519                     "password" to "password"
    520                     "name" to "Bat"
    521                     "tan_channels" to listOf(channel)
    522                 }
    523             }.assertConflict(TalerErrorCode.BANK_MISSING_TAN_INFO)
    524         }
    525         client.postAdmin("/accounts") {
    526             json { 
    527                 "username" to "bat2"
    528                 "password" to "password"
    529                 "name" to "Bat"
    530                 "tan_channels" to channels
    531             }
    532         }.assertConflict(TalerErrorCode.BANK_MISSING_TAN_INFO)
    533 
    534         // Check unknown conversion rate class
    535         client.postAdmin("/accounts") {
    536             json {
    537                 "username" to "new_account"
    538                 "password" to "password"
    539                 "name" to "New Account"
    540                 "conversion_rate_class_id" to 42
    541             }
    542         }.assertConflict(TalerErrorCode.BANK_CONVERSION_RATE_CLASS_UNKNOWN)
    543 
    544         // Reserved account
    545         RESERVED_ACCOUNTS.forEach {
    546             client.post("/accounts") {
    547                 json {
    548                     "username" to it
    549                     "password" to "password"
    550                     "name" to "John Smith"
    551                 }
    552             }.assertConflict(TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT)
    553         }
    554 
    555         // Malformed username
    556         listOf("bad@username", "long".repeat(50)).forEach {
    557             client.post("/accounts") {
    558                 json {
    559                     "username" to it
    560                     "password" to "password"
    561                     "name" to "John Smith"
    562                 }
    563             }.assertBadRequest()
    564         }
    565 
    566         // Non exchange account
    567         client.post("/accounts") {
    568             json {
    569                 "username" to "exchange"
    570                 "password" to "password"
    571                 "name" to "Exchange"
    572             }
    573         }.assertConflict(TalerErrorCode.END)
    574 
    575         // Testing username conflict
    576         client.post("/accounts") {
    577             json(req) {
    578                 "name" to "Foo"
    579             }
    580         }.assertConflict(TalerErrorCode.BANK_REGISTER_USERNAME_REUSE)
    581         // Testing payto conflict
    582         client.post("/accounts") {
    583             json(req) {
    584                 "username" to "bar"
    585             }
    586         }.assertConflict(TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE)
    587         client.getAdmin("/accounts/bar").assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT)
    588         // Testing bad payto kind
    589         client.post("/accounts") {
    590             json(req) {
    591                 "username" to "bar"
    592                 "password" to "bar-password"
    593                 "name" to "Mr Bar"
    594                 "payto_uri" to "payto://x-taler-bank/bank.hostname.test/bar"
    595             }
    596         }.assertBadRequest()
    597         // Testing short password
    598         client.post("/accounts") {
    599             json(req) {
    600                 "password" to "short"
    601             }
    602         }.assertConflict(TalerErrorCode.BANK_PASSWORD_TOO_SHORT)
    603         // Testing long password
    604         client.post("/accounts") {
    605             json(req) {
    606                 "password" to "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong-password"
    607             }
    608         }.assertConflict(TalerErrorCode.BANK_PASSWORD_TOO_LONG)
    609 
    610         // Check cashout payto receiver name logic
    611         client.post("/accounts") {
    612             json {
    613                 "username" to "cashout_guess"
    614                 "password" to "cashout_guess-password"
    615                 "name" to "Mr Guess My Name"
    616                 "cashout_payto_uri" to payto
    617             }
    618         }.assertOk()
    619         client.getA("/accounts/cashout_guess").assertOkJson<AccountData> {
    620             assertEquals(payto.full("Mr Guess My Name"), it.cashout_payto_uri)
    621         }
    622         client.post("/accounts") {
    623             json {
    624                 "username" to "cashout_keep"
    625                 "password" to "cashout_keep-password"
    626                 "name" to "Mr Keep My Name"
    627                 "cashout_payto_uri" to payto.full("Santa Claus")
    628             }
    629         }.assertOk()
    630         client.getA("/accounts/cashout_keep").assertOkJson<AccountData> {
    631             assertEquals(payto.full("Mr Keep My Name"), it.cashout_payto_uri)
    632         }
    633 
    634         // Check input restrictions
    635         obj {
    636             "username" to "username"
    637             "password" to "password"
    638             "name" to "Name"
    639         }.let { req ->
    640             client.post("/accounts") {
    641                 json(req) { "username" to "bad/username" }
    642             }.assertBadRequest()
    643             client.post("/accounts") {
    644                 json(req) { "username" to " spaces " }
    645             }.assertBadRequest()
    646             client.post("/accounts") {
    647                 json(req) {
    648                     "contact_data" to obj {
    649                         "phone" to " +456"
    650                     }
    651                 }
    652             }.assertBadRequest()
    653             client.post("/accounts") {
    654                 json(req) {
    655                     "contact_data" to obj {
    656                         "phone" to " test@mail.com"
    657                     }
    658                 }
    659             }.assertBadRequest()
    660         }
    661     }
    662 
    663     // Test account created with bonus
    664     @Test
    665     fun createBonus() = bankSetup(conf = "test_bonus.conf") {
    666         val req = obj {
    667             "username" to "foo"
    668             "password" to "password-xyz"
    669             "name" to "Mallory"
    670         }
    671 
    672         setMaxDebt("admin", "KUDOS:10000")
    673 
    674         // Check ok
    675         repeat(100) {
    676             client.postAdmin("/accounts") {
    677                 json(req) {
    678                     "username" to "foo$it"
    679                 }
    680             }.assertOk()
    681             assertBalance("foo$it", "+KUDOS:100")
    682         }
    683         assertBalance("admin", "-KUDOS:10000")
    684         
    685         // Check insufficient fund
    686         client.postAdmin("/accounts") {
    687             json(req) {
    688                 "username" to "bar"
    689             }
    690         }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT)
    691         client.getAdmin("/accounts/bar").assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT)
    692     }
    693 
    694     // Test admin-only account creation
    695     @Test
    696     fun createRestricted() = bankSetup(conf = "test_restrict.conf") { 
    697         authRoutine(HttpMethod.Post, "/accounts", requireAdmin = true)
    698         client.postAdmin("/accounts") {
    699             json {
    700                 "username" to "baz"
    701                 "password" to "password-xyz"
    702                 "name" to "Mallory"
    703             }
    704         }.assertOk()
    705     }
    706 
    707     // Test admin-only account creation
    708     @Test
    709     fun createTanErr() = bankSetup(conf = "test_tan_err.conf") { 
    710         client.postAdmin("/accounts") {
    711             json {
    712                 "username" to "baz"
    713                 "password" to "xyz"
    714                 "name" to "Mallory"
    715                 "tan_channel" to "email"
    716             }
    717         }.assertConflict(TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED)
    718     }
    719 
    720     // POST /accounts
    721     @Test
    722     fun createNoCheck() = bankSetup("test_no_password_check.conf") {
    723         // Testing short password
    724         client.post("/accounts") {
    725             json {
    726                 "username" to "short"
    727                 "name" to "John Smith"
    728                 "password" to "short"
    729             }
    730         }.assertOk()
    731         // Testing long password
    732         client.post("/accounts") {
    733             json {
    734                 "username" to "long"
    735                 "name" to "Jane Smith"
    736                 "password" to "loooooooooooooooooooooooooooooooooooooooooooooooooooooooong-password"
    737             }
    738         }.assertOk()
    739     }
    740 
    741     // DELETE /accounts/USERNAME
    742     @Test
    743     fun delete() = bankSetup { db -> 
    744         authRoutine(HttpMethod.Delete, "/accounts/merchant", allowAdmin = true)
    745 
    746         // Reserved account
    747         RESERVED_ACCOUNTS.forEach {
    748             client.deleteAdmin("/accounts/$it")
    749                 .assertConflict(TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT)
    750         }
    751         client.deleteA("/accounts/exchange")
    752             .assertConflict(TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT)
    753 
    754         client.post("/accounts") {
    755             json {
    756                 "username" to "john"
    757                 "password" to "john-password"
    758                 "name" to "John"
    759                 "payto_uri" to genTmpPayTo()
    760             }
    761         }.assertOk()
    762         fillTanInfo("john")
    763         // Fail to delete, due to a non-zero balance.
    764         tx("customer", "KUDOS:1", "john")
    765         client.deleteA("/accounts/john")
    766             .assertConflict(TalerErrorCode.BANK_ACCOUNT_BALANCE_NOT_ZERO)
    767         // Successful deletion
    768         tx("john", "KUDOS:1", "customer")
    769         client.deleteA("/accounts/john")
    770             .assertChallenge()
    771             .assertNoContent()
    772         // Account no longer exists
    773         client.deleteA("/accounts/john")
    774             .assertUnauthorized(TalerErrorCode.GENERIC_TOKEN_UNKNOWN)
    775         client.deleteAdmin("/accounts/john")
    776             .assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT)
    777     }
    778 
    779     @Test
    780     fun softDelete() = bankSetup { db -> 
    781         // Create all kind of operations
    782         val token = client.postPw("/accounts/customer/token") {
    783             json { "scope" to "readonly" }
    784         }.assertOkJson<TokenSuccessResponse>().access_token
    785         val tx_id = client.postA("/accounts/customer/transactions") {
    786             json {
    787                 "payto_uri" to "$exchangePayto?message=payout"
    788                 "amount" to "KUDOS:0.3"
    789             }
    790         }.assertOkJson<TransactionCreateResponse>().row_id
    791         val withdrawal_id = client.postA("/accounts/customer/withdrawals") {
    792             json { "amount" to "KUDOS:9.0" } 
    793         }.assertOkJson<BankAccountCreateWithdrawalResponse>().withdrawal_id
    794         fillCashoutInfo("customer")
    795         val cashout_id = client.postA("/accounts/customer/cashouts") {
    796             json {
    797                 "request_uid" to ShortHashCode.rand()
    798                 "amount_debit" to "KUDOS:1"
    799                 "amount_credit" to convert("KUDOS:1")
    800             }
    801         }.assertOkJson<CashoutResponse>().cashout_id
    802         fillTanInfo("customer")
    803         client.postA("/accounts/customer/transactions") {
    804             json {
    805                 "payto_uri" to "$exchangePayto?message=payout"
    806                 "amount" to "KUDOS:0.3"
    807             }
    808         }.assertAcceptedJson<ChallengeResponse>()
    809 
    810         // Delete account
    811         tx("merchant", "KUDOS:1", "customer")
    812         assertBalance("customer", "+KUDOS:0")
    813         client.deleteA("/accounts/customer")
    814             .assertChallenge()
    815             .assertNoContent()
    816         
    817         // Check account can no longer username
    818         client.delete("/accounts/customer/token") {
    819             headers[HttpHeaders.Authorization] = "Bearer $token"
    820         }.assertUnauthorized(TalerErrorCode.GENERIC_TOKEN_UNKNOWN)
    821         client.getA("/accounts/customer/transactions/$tx_id")
    822             .assertUnauthorized(TalerErrorCode.GENERIC_TOKEN_UNKNOWN)
    823         client.getA("/accounts/customer/cashouts/$cashout_id")
    824             .assertUnauthorized(TalerErrorCode.GENERIC_TOKEN_UNKNOWN)
    825         client.postA("/accounts/customer/withdrawals/$withdrawal_id/confirm")
    826             .assertUnauthorized(TalerErrorCode.GENERIC_TOKEN_UNKNOWN)
    827 
    828         // But admin can still see existing operations
    829         client.getAdmin("/accounts/customer/transactions/$tx_id")
    830             .assertOkJson<BankAccountTransactionInfo>()
    831         client.getAdmin("/accounts/customer/cashouts/$cashout_id")
    832             .assertOkJson<CashoutStatusResponse>()
    833         client.get("/withdrawals/$withdrawal_id")
    834             .assertOkJson<WithdrawalPublicInfo>()
    835 
    836         // GC
    837         db.gc.collect(Instant.now(), Duration.ZERO, Duration.ZERO, Duration.ZERO)
    838         client.getAdmin("/accounts/customer/transactions/$tx_id")
    839             .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
    840         client.getAdmin("/accounts/customer/cashouts/$cashout_id")
    841             .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
    842         client.get("/withdrawals/$withdrawal_id")
    843             .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
    844     }
    845 
    846     // Test admin-only account deletion
    847     @Test
    848     fun deleteRestricted() = bankSetup(conf = "test_restrict.conf") { 
    849         authRoutine(HttpMethod.Post, "/accounts", requireAdmin = true)
    850         // Exchange is still restricted
    851         client.deleteAdmin("/accounts/exchange") {
    852         }.assertConflict(TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT)
    853     }
    854 
    855     // Test delete exchange account
    856     @Test
    857     fun deleteNoConversion() = bankSetup(conf = "test_no_conversion.conf") { 
    858         // Exchange is no longer restricted
    859         client.deleteA("/accounts/exchange").assertNoContent()
    860     }
    861 
    862     suspend fun ApplicationTestBuilder.checkAdminOnly(
    863         req: JsonElement,
    864         error: TalerErrorCode
    865     ) {
    866         // Check restricted
    867         client.patchA("/accounts/merchant") {
    868             json(req)
    869         }.assertConflict(error)
    870         // Check admin always can
    871         client.patchAdmin("/accounts/merchant") {
    872             json(req)
    873         }.assertNoContent()
    874         // Check idempotent
    875         client.patchA("/accounts/merchant") {
    876             json(req)
    877         }.assertNoContent()
    878     }
    879 
    880     // PATCH /accounts/USERNAME
    881     @Test
    882     fun reconfig() = bankSetup { 
    883         authRoutine(HttpMethod.Patch, "/accounts/merchant", allowAdmin = true)
    884 
    885         // Check tan info
    886         val channels = listOf("sms", "email")
    887         for (channel in channels) {
    888             client.patchA("/accounts/merchant") {
    889                 json { "tan_channel" to channel }
    890             }.assertConflict(TalerErrorCode.BANK_MISSING_TAN_INFO)
    891             client.patchA("/accounts/merchant") {
    892                 json { "tan_channels" to listOf(channel) }
    893             }.assertConflict(TalerErrorCode.BANK_MISSING_TAN_INFO)
    894         }
    895         client.patchA("/accounts/merchant") {
    896             json { "tan_channels" to channels }
    897         }.assertConflict(TalerErrorCode.BANK_MISSING_TAN_INFO)
    898 
    899         // Successful attempt now
    900         val cashout = IbanPayto.rand()
    901         val req = obj {
    902             "cashout_payto_uri" to cashout
    903             "name" to "Roger"
    904             "is_public" to true
    905             "contact_data" to obj {
    906                 "phone" to "+99"
    907                 "email" to "foo@example.com"
    908             }
    909         }
    910         client.patchA("/accounts/merchant") {
    911             json(req)
    912         }.assertNoContent()
    913         // Checking idempotence
    914         client.patchA("/accounts/merchant") {
    915             json(req)
    916         }.assertNoContent()
    917 
    918         checkAdminOnly(
    919             obj(req) { "debit_threshold" to "KUDOS:100" },
    920             TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT
    921         )
    922         createConversionRateClass()
    923         checkAdminOnly(
    924             obj(req) { "conversion_rate_class_id" to 1 },
    925             TalerErrorCode.BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS
    926         )
    927 
    928         // Check unknown conversion rate class
    929         client.patchAdmin("/accounts/merchant") {
    930             json(req) { "conversion_rate_class_id" to 42}
    931         }.assertConflict(TalerErrorCode.BANK_CONVERSION_RATE_CLASS_UNKNOWN)
    932         
    933         // Check currency
    934         client.patchAdmin("/accounts/merchant") {
    935             json(req) { "debit_threshold" to "EUR:100" }
    936         }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH)
    937 
    938         // Check patch
    939         client.getA("/accounts/merchant").assertOkJson<AccountData> { obj ->
    940             assertEquals("Roger", obj.name)
    941             assertEquals(cashout.full(obj.name), obj.cashout_payto_uri)
    942             assertEquals("+99", obj.contact_data?.phone?.get())
    943             assertEquals("foo@example.com", obj.contact_data?.email?.get())
    944             assertEquals(TalerAmount("KUDOS:100"), obj.debit_threshold)
    945             assert(obj.is_public)
    946             assert(!obj.is_taler_exchange)
    947         }
    948 
    949         // Check keep values when there is no changes
    950         client.patchA("/accounts/merchant") {
    951             json { }
    952         }.assertNoContent()
    953         client.getA("/accounts/merchant").assertOkJson<AccountData> { obj ->
    954             assertEquals("Roger", obj.name)
    955             assertEquals(cashout.full(obj.name), obj.cashout_payto_uri)
    956             assertEquals("+99", obj.contact_data?.phone?.get())
    957             assertEquals("foo@example.com", obj.contact_data?.email?.get())
    958             assertEquals(TalerAmount("KUDOS:100"), obj.debit_threshold)
    959             assert(obj.is_public)
    960             assert(!obj.is_taler_exchange)
    961         }
    962 
    963         // Admin cannot be public
    964         client.patchA("/accounts/admin") {
    965             json {
    966                 "is_public" to true
    967             }
    968         }.assertConflict(TalerErrorCode.END)
    969 
    970         // Exchange must be exchange
    971         client.patchA("/accounts/exchange") {
    972             json {
    973                 "is_taler_exchange" to false
    974             }
    975         }.assertConflict(TalerErrorCode.END)
    976 
    977         // Check cashout payto receiver name logic
    978         client.post("/accounts") {
    979             json {
    980                 "username" to "cashout"
    981                 "password" to "cashout-password"
    982                 "name" to "Mr Cashout Cashout"
    983             }
    984         }.assertOk()
    985         val canonical = Payto.parse(cashout.canonical).expectIban()
    986         for ((cashout, name, expect) in listOf(
    987             Triple(cashout.canonical, null, canonical.full("Mr Cashout Cashout")),
    988             Triple(cashout.canonical, "New name", canonical.full("New name")),
    989             Triple(cashout.full("Full name"), null, cashout.full("New name")),
    990             Triple(cashout.full("Full second name"), "Another name", cashout.full("Another name"))
    991         )) {
    992             client.patchAdmin("/accounts/cashout") {
    993                 json {
    994                     "cashout_payto_uri" to cashout
    995                     if (name != null) "name" to name
    996                 }
    997             }.assertNoContent()
    998             client.getA("/accounts/cashout").assertOkJson<AccountData> { obj ->
    999                 assertEquals(expect, obj.cashout_payto_uri)
   1000             }
   1001         }
   1002 
   1003         // Check 2FA
   1004         fillTanInfo("merchant")
   1005         client.patchA("/accounts/merchant") {
   1006             json { "is_public" to false }
   1007         }.assertChallenge {
   1008             client.getA("/accounts/merchant").assertOkJson<AccountData> { obj ->
   1009                 assert(obj.is_public)
   1010             }
   1011         }.assertNoContent()
   1012         client.getA("/accounts/merchant").assertOkJson<AccountData> { obj ->
   1013             assert(!obj.is_public)
   1014         }
   1015     }
   1016 
   1017     // Test admin-only account patch
   1018     @Test
   1019     fun patchRestricted() = bankSetup(conf = "test_restrict.conf") { 
   1020         // Check restricted
   1021         checkAdminOnly(
   1022             obj { "name" to "Another Foo" },
   1023             TalerErrorCode.BANK_NON_ADMIN_PATCH_LEGAL_NAME
   1024         )
   1025         checkAdminOnly(
   1026             obj { "cashout_payto_uri" to IbanPayto.rand() },
   1027             TalerErrorCode.BANK_NON_ADMIN_PATCH_CASHOUT
   1028         )
   1029         // Check idempotent
   1030         client.getA("/accounts/merchant").assertOkJson<AccountData> { obj ->
   1031             client.patchA("/accounts/merchant") {
   1032                 json {
   1033                     "name" to obj.name
   1034                     "cashout_payto_uri" to obj.cashout_payto_uri
   1035                     "debit_threshold" to obj.debit_threshold
   1036                 }
   1037             }.assertNoContent()
   1038         }
   1039     }
   1040 
   1041     // Test TAN check account patch
   1042     @Test
   1043     fun patchTanErr() = bankSetup(conf = "test_tan_err.conf") { 
   1044         // Check unsupported TAN channel
   1045         client.patchA("/accounts/customer") {
   1046             json {
   1047                 "tan_channel" to "email"
   1048             }
   1049         }.assertConflict(TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED)
   1050     }
   1051 
   1052     // PATCH /accounts/USERNAME/auth
   1053     @Test
   1054     fun passwordChange() = bankSetup { 
   1055         authRoutine(HttpMethod.Patch, "/accounts/merchant/auth", allowAdmin = true)
   1056 
   1057         // Changing the password.
   1058         client.patchA("/accounts/customer/auth") {
   1059             json {
   1060                 "old_password" to "customer-password"
   1061                 "new_password" to "new-password"
   1062             }
   1063         }.assertNoContent()
   1064         // Previous password should fail.
   1065         client.post("/accounts/customer/token") {
   1066             basicAuth("customer", "customer-password")
   1067         }.assertUnauthorized()
   1068         // New password should succeed.
   1069         client.post("/accounts/customer/token") {
   1070             basicAuth("customer", "new-password")
   1071             json { "scope" to "readonly" }
   1072         }.assertOk()
   1073         client.patchA("/accounts/customer/auth") {
   1074             json {
   1075                 "old_password" to "new-password"
   1076                 "new_password" to "customer-password"
   1077             }
   1078         }.assertNoContent()
   1079 
   1080 
   1081         // Check require test old password
   1082         client.patchA("/accounts/customer/auth") {
   1083             json {
   1084                 "old_password" to "bad-password"
   1085                 "new_password" to "new-password"
   1086             }
   1087         }.assertConflict(TalerErrorCode.BANK_PATCH_BAD_OLD_PASSWORD)
   1088 
   1089         // Check require old password for user
   1090         client.patchA("/accounts/customer/auth") {
   1091             json {
   1092                 "new_password" to "new-password"
   1093             }
   1094         }.assertConflict(TalerErrorCode.BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD)
   1095         // Testing short password
   1096         client.patchA("/accounts/merchant/auth") {
   1097             json {
   1098                 "old_password" to "ignored"
   1099                 "new_password" to "short"
   1100             }
   1101         }.assertConflict(TalerErrorCode.BANK_PASSWORD_TOO_SHORT)
   1102         // Testing long password
   1103         client.patchA("/accounts/merchant/auth") {
   1104             json {
   1105                 "old_password" to "ignored"
   1106                 "new_password" to "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong-password"
   1107             }
   1108         }.assertConflict(TalerErrorCode.BANK_PASSWORD_TOO_LONG)
   1109 
   1110         // Check admin 
   1111         client.patchAdmin("/accounts/customer/auth") {
   1112             json {
   1113                 "new_password" to "customer-password"
   1114             }
   1115         }.assertNoContent()
   1116 
   1117         // Check 2FA
   1118         fillTanInfo("customer")
   1119         client.patchA("/accounts/customer/auth") {
   1120             json {
   1121                 "old_password" to "customer-password"
   1122                 "new_password" to "it-password"
   1123             }
   1124         }.assertChallenge().assertNoContent()
   1125         client.patchAdmin("/accounts/customer/auth") {
   1126             json {
   1127                 "new_password" to "new-password"
   1128             }
   1129         }.assertNoContent()
   1130    
   1131         
   1132         // Check 2FA after password check
   1133         client.patchA("/accounts/customer/auth") {
   1134             json {
   1135                 "old_password" to "password"
   1136                 "new_password" to "new-password"
   1137             }
   1138         }.assertConflict(TalerErrorCode.BANK_PATCH_BAD_OLD_PASSWORD)
   1139     }
   1140 
   1141     // PATCH /accounts/USERNAME/auth
   1142     @Test
   1143     fun passwordChangeNoCheck() = bankSetup("test_no_password_check.conf") {
   1144         // Testing short password
   1145         client.patchA("/accounts/merchant/auth") {
   1146             json {
   1147                 "old_password" to "merchant-password"
   1148                 "new_password" to "short"
   1149             }
   1150         }.assertNoContent()
   1151         // Testing long password
   1152         client.patchA("/accounts/merchant/auth") {
   1153             json {
   1154                 "old_password" to "short"
   1155                 "new_password" to "looooooooooooooooooooooooooooooooooooooooooooooooooooooooong-password"
   1156             }
   1157         }.assertNoContent()
   1158     }
   1159 
   1160     // GET /public-accounts and GET /accounts
   1161     @Test
   1162     fun list() = bankSetup(conf = "test_no_conversion.conf") { db -> 
   1163         authRoutine(HttpMethod.Get, "/accounts", requireAdmin = true)
   1164         // Remove default accounts
   1165         val defaultAccounts = listOf("merchant", "exchange", "customer")
   1166         defaultAccounts.forEach {
   1167             client.deleteAdmin("/accounts/$it").assertNoContent()
   1168         }
   1169         client.getAdmin("/accounts").assertOkJson<ListBankAccountsResponse> {
   1170             for (account in it.accounts) {
   1171                 assertNull(account.conversion_rate)
   1172                 if (defaultAccounts.contains(account.username)) {
   1173                     assertEquals(AccountStatus.deleted, account.status)
   1174                 } else {
   1175                     assertEquals(AccountStatus.active, account.status)
   1176                 }
   1177             }
   1178         }
   1179         db.gc.collect(Instant.now(), Duration.ZERO, Duration.ZERO, Duration.ZERO)
   1180         // Check error when no public accounts
   1181         client.get("/public-accounts").assertNoContent()
   1182         client.getAdmin("/accounts").assertOkJson<ListBankAccountsResponse>()
   1183     }
   1184 
   1185     @Test
   1186     fun listConversionClass() = bankSetup(conf = "test.conf") { db ->
   1187         repeat(3) {
   1188            createConversionRateClass()
   1189         }
   1190         
   1191         // Gen some public and private accounts
   1192         repeat(5) {
   1193             client.postAdmin("/accounts") {
   1194                 val mod = it%3
   1195                 val rateClassId = if (mod in 1..3) mod else null
   1196                 json {
   1197                     "username" to "$it"
   1198                     "password" to "password"
   1199                     "name" to "Mr 1$it"
   1200                     "is_public" to (it%2 == 0)
   1201                     "conversion_rate_class_id" to rateClassId
   1202                 }
   1203             }.assertOk()
   1204         }
   1205         // All public
   1206         client.get("/public-accounts").assertOkJson<PublicAccountsResponse> {
   1207             assertEquals(3, it.public_accounts.size)
   1208             it.public_accounts.forEach {
   1209                 assertEquals(0, (it.username.toInt() - 10) % 2)
   1210             }
   1211         }
   1212         // Conversion rate
   1213         client.getAdmin("/accounts").assertOkJson<ListBankAccountsResponse> {
   1214             for (account in it.accounts) {
   1215                 val rate = client.getAdmin("/accounts/${account.username}/conversion-info/rate").assertOkJson<ConversionRate>()
   1216                 assertEquals(account.conversion_rate, rate)
   1217             }
   1218         }
   1219         // Filtering
   1220         suspend fun checkIds(query: String, vararg ids: String) {
   1221             val res = client.getAdmin("/accounts?$query")
   1222             val list = listOf(*ids)
   1223             if (list.isEmpty()) {
   1224                 res.assertNoContent()
   1225             } else {
   1226                 res.assertOkJson<ListBankAccountsResponse> {
   1227                     assertEquals(list, it.accounts.map { it.username })
   1228                 }
   1229             }
   1230         }
   1231         checkIds("", "4", "3", "2", "1", "0", "admin", "customer", "exchange", "merchant")
   1232         checkIds("filter_name=1", "4", "3", "2", "1", "0")
   1233         checkIds("filter_name=3", "3")
   1234         checkIds("conversion_rate_class_id=1", "4", "1")
   1235         checkIds("conversion_rate_class_id=2", "2")
   1236         checkIds("conversion_rate_class_id=3")
   1237         checkIds("conversion_rate_class_id=4")
   1238         checkIds("conversion_rate_class_id=0", "3", "0", "admin", "customer", "exchange", "merchant")
   1239         checkIds("conversion_rate_class_id=0&filter_name=1", "3", "0")
   1240         for ((id, num) in mapOf(1 to 2, 2 to 1, 3 to 0)) {
   1241             client.getAdmin("/conversion-rate-classes/$id").assertOkJson<ConversionRateClass> {
   1242                 assertEquals(it.num_users, num)
   1243             }
   1244         }
   1245     }
   1246 
   1247     // GET /accounts/USERNAME
   1248     @Test
   1249     fun get() = bankSetup { 
   1250         authRoutine(HttpMethod.Get, "/accounts/merchant", allowAdmin = true)
   1251         // Check ok
   1252         client.getA("/accounts/merchant").assertOkJson<AccountData> {
   1253             assertEquals("Merchant", it.name)
   1254         }
   1255     }
   1256 }
   1257 
   1258 class CoreBankTransactionsApiTest {
   1259     // GET /transactions
   1260     @Test
   1261     fun history() = bankSetup { 
   1262         authRoutine(HttpMethod.Get, "/accounts/merchant/transactions", allowAdmin = true)
   1263         historyRoutine<BankAccountTransactionsResponse>(
   1264             url = "/accounts/customer/transactions",
   1265             ids = { it.transactions.map { it.row_id } },
   1266             registered = listOf(
   1267                 { 
   1268                     // Transactions from merchant to exchange
   1269                     tx("merchant", "KUDOS:0.1", "customer")
   1270                 },
   1271                 { 
   1272                     // Transactions from exchange to merchant
   1273                     tx("customer", "KUDOS:0.1", "merchant")
   1274                 },
   1275                 { 
   1276                     // Transactions from merchant to exchange
   1277                     tx("merchant", "KUDOS:0.1", "customer")
   1278                 },
   1279                 { 
   1280                     // Cashout from merchant
   1281                     cashout("KUDOS:0.1")
   1282                 }
   1283             ),
   1284             ignored = listOf(
   1285                 {
   1286                     // Ignore transactions of other accounts
   1287                     tx("merchant", "KUDOS:0.1", "exchange")
   1288                 },
   1289                 {
   1290                     // Ignore transactions of other accounts
   1291                     tx("exchange", "KUDOS:0.1", "merchant")
   1292                 }
   1293             )
   1294         )
   1295     }
   1296 
   1297     // GET /transactions/T_ID
   1298     @Test
   1299     fun testById() = bankSetup { 
   1300         authRoutine(HttpMethod.Get, "/accounts/merchant/transactions/42", allowAdmin = true)
   1301 
   1302         // Create transaction
   1303         tx("merchant", "KUDOS:0.3", "exchange", "tx")
   1304         // Check OK
   1305         client.getA("/accounts/merchant/transactions/1")
   1306             .assertOkJson<BankAccountTransactionInfo> { tx ->
   1307             assertEquals("tx", tx.subject)
   1308             assertEquals(TalerAmount("KUDOS:0.3"), tx.amount)
   1309         }
   1310         // Check unknown transaction
   1311         client.getA("/accounts/merchant/transactions/3")
   1312             .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
   1313         // Check another user's transaction
   1314         client.getA("/accounts/merchant/transactions/2")
   1315             .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
   1316     }
   1317 
   1318     // POST /transactions
   1319     @Test
   1320     fun create() = bankSetup { db -> 
   1321         authRoutine(HttpMethod.Post, "/accounts/merchant/transactions")
   1322 
   1323         val valid_req = obj {
   1324             "payto_uri" to "$exchangePayto?message=payout"
   1325             "amount" to "KUDOS:0.3"
   1326         }
   1327 
   1328         // Check OK
   1329         client.postA("/accounts/merchant/transactions") {
   1330             json(valid_req)
   1331         }.assertOkJson<TransactionCreateResponse> {
   1332             client.getA("/accounts/merchant/transactions/${it.row_id}")
   1333                 .assertOkJson<BankAccountTransactionInfo> { tx ->
   1334                 assertEquals("payout", tx.subject)
   1335                 assertEquals(TalerAmount("KUDOS:0.3"), tx.amount)
   1336             }
   1337         }
   1338 
   1339         // Check idempotency
   1340         ShortHashCode.rand().let { requestUid ->
   1341             val id = client.postA("/accounts/merchant/transactions") {
   1342                 json(valid_req) {
   1343                     "request_uid" to requestUid
   1344                 }
   1345             }.assertOkJson<TransactionCreateResponse>().row_id
   1346             client.postA("/accounts/merchant/transactions") {
   1347                 json(valid_req) {
   1348                     "request_uid" to requestUid
   1349                 }
   1350             }.assertOkJson<TransactionCreateResponse> {
   1351                 assertEquals(id, it.row_id)
   1352             }
   1353             client.postA("/accounts/merchant/transactions") {
   1354                 json(valid_req) {
   1355                     "request_uid" to requestUid
   1356                     "amount" to "KUDOS:42"
   1357                 }
   1358             }.assertConflict(TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED)
   1359         }
   1360         
   1361         // Check amount in payto_uri
   1362         client.postA("/accounts/merchant/transactions") {
   1363             json {
   1364                 "payto_uri" to "$exchangePayto?message=payout2&amount=KUDOS:1.05"
   1365             }
   1366         }.assertOkJson <TransactionCreateResponse> {
   1367             client.getA("/accounts/merchant/transactions/${it.row_id}")
   1368                 .assertOkJson<BankAccountTransactionInfo> { tx ->
   1369                 assertEquals("payout2", tx.subject)
   1370                 assertEquals(TalerAmount("KUDOS:1.05"), tx.amount)
   1371             }
   1372         }
   1373        
   1374         // Check amount in payto_uri precedence
   1375         client.postA("/accounts/merchant/transactions") {
   1376             json {
   1377                 "payto_uri" to "$exchangePayto?message=payout3&amount=KUDOS:1.05"
   1378                 "amount" to "KUDOS:10.003"
   1379             }
   1380         }.assertOkJson<TransactionCreateResponse> {
   1381             client.getA("/accounts/merchant/transactions/${it.row_id}")
   1382                 .assertOkJson<BankAccountTransactionInfo> { tx ->
   1383                 assertEquals("payout3", tx.subject)
   1384                 assertEquals(TalerAmount("KUDOS:1.05"), tx.amount)
   1385             }
   1386         }
   1387         // Testing the wrong currency
   1388         client.postA("/accounts/merchant/transactions") {
   1389             json(valid_req) {
   1390                 "amount" to "EUR:3.3"
   1391             }
   1392         }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH)
   1393         // Surpassing the debt limit
   1394         client.postA("/accounts/merchant/transactions") {
   1395             json(valid_req) {
   1396                 "amount" to "KUDOS:555"
   1397             }
   1398         }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT)
   1399         // Missing message
   1400         client.postA("/accounts/merchant/transactions") {
   1401             json(valid_req) {
   1402                 "payto_uri" to "$exchangePayto"
   1403             }
   1404         }.assertBadRequest()
   1405         // Unknown creditor
   1406         client.postA("/accounts/merchant/transactions") {
   1407             json(valid_req) {
   1408                 "payto_uri" to "$unknownPayto?message=payout"
   1409             }
   1410         }.assertConflict(TalerErrorCode.BANK_UNKNOWN_CREDITOR)
   1411         // Transaction to self
   1412         client.postA("/accounts/merchant/transactions") {
   1413             json(valid_req) {
   1414                 "payto_uri" to "$merchantPayto?message=payout"
   1415             }
   1416         }.assertConflict(TalerErrorCode.BANK_SAME_ACCOUNT)
   1417         // Transaction to admin
   1418         val adminPayto = client.getA("/accounts/admin")
   1419             .assertOkJson<AccountData>().payto_uri
   1420         client.postA("/accounts/merchant/transactions") {
   1421             json(valid_req) {
   1422                 "payto_uri" to "$adminPayto&message=payout"
   1423             }
   1424         }.assertConflict(TalerErrorCode.BANK_ADMIN_CREDITOR)
   1425 
   1426         // Init state
   1427         assertBalance("merchant", "+KUDOS:0")
   1428         assertBalance("customer", "+KUDOS:0")
   1429         // Send 2 times 3
   1430         repeat(2) {
   1431             tx("merchant", "KUDOS:3", "customer")
   1432         }
   1433         client.postA("/accounts/merchant/transactions") {
   1434             json {
   1435                 "payto_uri" to "$customerPayto?message=payout2&amount=KUDOS:5"
   1436             }
   1437         }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT)
   1438         assertBalance("merchant", "-KUDOS:6")
   1439         assertBalance("customer", "+KUDOS:6")
   1440         // Send through debt
   1441         tx("customer", "KUDOS:10", "merchant")
   1442         assertBalance("merchant", "+KUDOS:4")
   1443         assertBalance("customer", "-KUDOS:4")
   1444         tx("merchant", "KUDOS:4", "customer")
   1445 
   1446         // Check bounce
   1447         assertBalance("merchant", "+KUDOS:0")
   1448         assertBalance("exchange", "+KUDOS:0")
   1449         tx("merchant", "KUDOS:1", "exchange", "") // Bounce common to transaction
   1450         tx("merchant", "KUDOS:1", "exchange", "Malformed") // Bounce malformed transaction
   1451         tx("merchant", "KUDOS:1", "exchange", "ADMIN BALANCE ADJUST") // Bounce admin balance adjust
   1452         val reservePub = EddsaPublicKey.randEdsaKey()
   1453         tx("merchant", "KUDOS:1", "exchange", randIncomingSubject(reservePub)) // Accept incoming
   1454         tx("merchant", "KUDOS:1", "exchange", randIncomingSubject(reservePub)) // Bounce reserve_pub reuse
   1455         assertBalance("merchant", "-KUDOS:1")
   1456         assertBalance("exchange", "+KUDOS:1")
   1457         
   1458         // Check warn
   1459         assertBalance("merchant", "-KUDOS:1")
   1460         assertBalance("exchange", "+KUDOS:1")
   1461         tx("exchange", "KUDOS:1", "merchant", "") // Warn common to transaction
   1462         tx("exchange", "KUDOS:1", "merchant", "Malformed") // Warn malformed transaction
   1463         val wtid = ShortHashCode.rand()
   1464         val exchange = BaseURL.parse("http://exchange.example.com/")
   1465         tx("exchange", "KUDOS:1", "merchant", randOutgoingSubject(wtid, exchange)) // Accept outgoing
   1466         tx("exchange", "KUDOS:1", "merchant", randOutgoingSubject(wtid, exchange)) // Warn wtid reuse
   1467         assertBalance("merchant", "+KUDOS:3")
   1468         assertBalance("exchange", "-KUDOS:3")
   1469 
   1470         // Check 2fa
   1471         fillTanInfo("merchant")
   1472         assertBalance("merchant", "+KUDOS:3")
   1473         assertBalance("customer", "+KUDOS:0")
   1474         client.postA("/accounts/merchant/transactions") {
   1475             json {
   1476                 "payto_uri" to "$customerPayto?message=tan+check&amount=KUDOS:1"
   1477             }
   1478         }.assertChallenge {
   1479             assertBalance("merchant", "+KUDOS:3")
   1480             assertBalance("customer", "+KUDOS:0")
   1481         }.assertOkJson <TransactionCreateResponse> { 
   1482             assertBalance("merchant", "+KUDOS:2")
   1483             assertBalance("customer", "+KUDOS:1")
   1484         }
   1485 
   1486         // Check 2fa idempotency
   1487         val req = obj {
   1488             "payto_uri" to "$customerPayto?message=tan+check&amount=KUDOS:1"
   1489             "request_uid" to ShortHashCode.rand()
   1490         }
   1491         val id = client.postA("/accounts/merchant/transactions") {
   1492             json(req)
   1493         }.assertChallenge {
   1494             assertBalance("merchant", "+KUDOS:2")
   1495             assertBalance("customer", "+KUDOS:1")
   1496         }.assertOkJson <TransactionCreateResponse> { 
   1497             assertBalance("merchant", "+KUDOS:1")
   1498             assertBalance("customer", "+KUDOS:2")
   1499         }.row_id
   1500         client.postA("/accounts/merchant/transactions") {
   1501             json(req)
   1502         }.assertOkJson<TransactionCreateResponse> {
   1503             assertEquals(id, it.row_id)
   1504         }
   1505         client.postA("/accounts/merchant/transactions") {
   1506             json(req) {
   1507                 "payto_uri" to "$customerPayto?message=tan+chec2k&amount=KUDOS:1"
   1508             }
   1509         }.assertConflict(TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED)
   1510     }
   1511 
   1512     @Test
   1513     fun createWithFee() = bankSetup(conf = "test_with_fees.conf") {
   1514         // Init state
   1515         assertBalance("merchant", "+KUDOS:0")
   1516         assertBalance("customer", "+KUDOS:0")
   1517         assertBalance("admin", "+KUDOS:0")
   1518 
   1519         // Check fee are sent to admin
   1520         tx("merchant", "KUDOS:3", "customer")
   1521         assertBalance("merchant", "-KUDOS:3.1")
   1522         assertBalance("customer", "+KUDOS:3")
   1523         assertBalance("admin", "+KUDOS:0.1")
   1524 
   1525         // Check amount with fee and min & max are checked
   1526         for (amount in listOf("KUDOS:7", "KUDOS:6.9", "KUDOS:0", "KUDOS:150")) {
   1527             client.postA("/accounts/merchant/transactions") {
   1528                 json {
   1529                     "payto_uri" to "$customerPayto?message=payout2&amount=$amount"
   1530                 }
   1531             }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT)
   1532         }
   1533         // Check empty account
   1534         tx("merchant", "KUDOS:6.8", "customer")
   1535         assertBalance("merchant", "-KUDOS:10")
   1536         assertBalance("customer", "+KUDOS:9.8")
   1537         assertBalance("admin", "+KUDOS:0.2")
   1538 
   1539         // Admin check no fee
   1540         tx("admin", "KUDOS:0.35", "merchant")
   1541         assertBalance("merchant", "-KUDOS:9.65")
   1542         assertBalance("admin", "-KUDOS:0.15")
   1543 
   1544         // Admin recover from debt
   1545         tx("customer", "KUDOS:1", "merchant")
   1546         assertBalance("admin", "-KUDOS:0.05")
   1547         tx("customer", "KUDOS:1", "merchant")
   1548         assertBalance("merchant", "-KUDOS:7.65")
   1549         assertBalance("customer", "+KUDOS:7.6")
   1550         assertBalance("admin", "+KUDOS:0.05")
   1551     }
   1552 }
   1553 
   1554 class CoreBankWithdrawalApiTest {
   1555     // POST /accounts/USERNAME/withdrawals
   1556     @Test
   1557     fun create() = bankSetup {
   1558         authRoutine(HttpMethod.Post, "/accounts/merchant/withdrawals")
   1559         
   1560         // Check OK
   1561         for (valid in listOf(
   1562             obj {}, 
   1563             obj { "amount" to "KUDOS:1.0" },
   1564             obj { "suggested_amount" to "KUDOS:2.0" }, 
   1565             obj {
   1566                 "amount" to "KUDOS:3.0"
   1567                 "suggested_amount" to "KUDOS:4.0"
   1568             }
   1569         )) {
   1570             // Check OK
   1571             client.postA("/accounts/merchant/withdrawals") {
   1572                 json(valid)
   1573             }.assertOkJson<BankAccountCreateWithdrawalResponse> {
   1574                 assertEquals("taler+http://withdraw/localhost:8080/taler-integration/${it.withdrawal_id}", it.taler_withdraw_uri)
   1575             }
   1576         }
   1577 
   1578         // Check exchange account
   1579         client.postA("/accounts/exchange/withdrawals") {
   1580             json { "amount" to "KUDOS:9.0" } 
   1581         }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE)
   1582 
   1583         // Check insufficient fund
   1584         client.postA("/accounts/merchant/withdrawals") {
   1585             json { "amount" to "KUDOS:90" } 
   1586         }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT)
   1587         client.postA("/accounts/merchant/withdrawals") {
   1588             json { "suggested_amount" to "KUDOS:90" } 
   1589         }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT)
   1590 
   1591         // Check wrong currency
   1592         client.postA("/accounts/merchant/withdrawals") {
   1593             json { "amount" to "EUR:90" } 
   1594         }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH)
   1595         client.postA("/accounts/merchant/withdrawals") {
   1596             json { "suggested_amount" to "EUR:90" } 
   1597         }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH)
   1598     }
   1599     
   1600     @Test
   1601     fun createWithFee() = bankSetup(conf = "test_with_fees.conf") {
   1602         // Check insufficient fund
   1603         for (amount in listOf("KUDOS:11", "KUDOS:10", "KUDOS:0", "KUDOS:150")) {
   1604             for (name in listOf("amount", "suggested_amount")) {
   1605                 client.postA("/accounts/merchant/withdrawals") {
   1606                     json { name to amount } 
   1607                 }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT)
   1608             }
   1609         }
   1610 
   1611         // Check OK
   1612         for (name in listOf("amount", "suggested_amount")) {
   1613             client.postA("/accounts/merchant/withdrawals") {
   1614                 json { name to "KUDOS:9.9" } 
   1615             }.assertOk()
   1616         }
   1617     }
   1618 
   1619     // GET /withdrawals/withdrawal_id
   1620     @Test
   1621     fun get() = bankSetup {
   1622         // Check OK
   1623         for (valid in listOf(
   1624             Pair(null, null),
   1625             Pair("KUDOS:1.0", null),
   1626             Pair(null, "KUDOS:2.0") ,
   1627             Pair("KUDOS:3.0", "KUDOS:4.0")
   1628         )) {
   1629             val amount = valid.first?.run(::TalerAmount)
   1630             val suggested = valid.second?.run(::TalerAmount)
   1631             client.postA("/accounts/merchant/withdrawals") {
   1632                 json { 
   1633                     "amount" to amount
   1634                     "suggested_amount" to suggested
   1635                 }
   1636             }.assertOkJson<BankAccountCreateWithdrawalResponse> {
   1637                 client.get("/withdrawals/${it.withdrawal_id}")
   1638                     .assertOkJson<WithdrawalPublicInfo> {
   1639                     assertEquals(amount, it.amount)
   1640                     assertEquals(suggested, it.suggested_amount)
   1641                 }
   1642             }
   1643         }
   1644 
   1645         // Check polling
   1646         statusRoutine<WithdrawalPublicInfo>("/withdrawals") { it.status }
   1647 
   1648         // Check bad UUID
   1649         client.get("/withdrawals/chocolate").assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED)
   1650 
   1651         // Check unknown
   1652         client.get("/withdrawals/${UUID.randomUUID()}")
   1653             .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
   1654     }
   1655 
   1656     // POST /accounts/USERNAME/withdrawals/withdrawal_id/abort
   1657     @Test
   1658     fun abort() = bankSetup {
   1659         authRoutine(HttpMethod.Post, "/accounts/merchant/withdrawals/42/abort")
   1660 
   1661         // Check abort created
   1662         client.postA("/accounts/merchant/withdrawals") {
   1663             json { "amount" to "KUDOS:1" } 
   1664         }.assertOkJson<BankAccountCreateWithdrawalResponse> {
   1665             val uuid = it.withdrawal_id
   1666 
   1667             // Check OK
   1668             client.postA("/accounts/merchant/withdrawals/$uuid/abort").assertNoContent()
   1669             // Check idempotence
   1670             client.postA("/accounts/merchant/withdrawals/$uuid/abort").assertNoContent()
   1671         }
   1672 
   1673         // Check abort selected
   1674         client.postA("/accounts/merchant/withdrawals") {
   1675             json { "amount" to "KUDOS:1" } 
   1676         }.assertOkJson<BankAccountCreateWithdrawalResponse> {
   1677             val uuid = it.withdrawal_id
   1678             withdrawalSelect(uuid)
   1679 
   1680             // Check OK
   1681             client.postA("/accounts/merchant/withdrawals/$uuid/abort").assertNoContent()
   1682             // Check idempotence
   1683             client.postA("/accounts/merchant/withdrawals/$uuid/abort").assertNoContent()
   1684         }
   1685 
   1686         // Check abort confirmed
   1687         client.postA("/accounts/merchant/withdrawals") {
   1688             json { "amount" to "KUDOS:1" } 
   1689         }.assertOkJson<BankAccountCreateWithdrawalResponse> {
   1690             val uuid = it.withdrawal_id
   1691             withdrawalSelect(uuid)
   1692             client.postA("/accounts/merchant/withdrawals/$uuid/confirm").assertNoContent()
   1693 
   1694             // Check error
   1695             client.postA("/accounts/merchant/withdrawals/$uuid/abort")
   1696                 .assertConflict(TalerErrorCode.BANK_ABORT_CONFIRM_CONFLICT)
   1697         }
   1698 
   1699         // Check confirm another user's operation
   1700         client.postA("/accounts/customer/withdrawals") {
   1701             json { "amount" to "KUDOS:1" } 
   1702         }.assertOkJson<BankAccountCreateWithdrawalResponse> {
   1703             val uuid = it.withdrawal_id
   1704             withdrawalSelect(uuid)
   1705 
   1706             // Check error
   1707             client.postA("/accounts/merchant/withdrawals/$uuid/abort")
   1708                 .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
   1709         }
   1710 
   1711         // Check bad UUID
   1712         client.postA("/accounts/merchant/withdrawals/chocolate/abort")
   1713             .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED)
   1714 
   1715         // Check unknown
   1716         client.postA("/accounts/merchant/withdrawals/${UUID.randomUUID()}/abort")
   1717             .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
   1718     }
   1719 
   1720     // POST /accounts/USERNAME/withdrawals/withdrawal_id/confirm
   1721     @Test
   1722     fun confirm() = bankSetup { 
   1723         authRoutine(HttpMethod.Post, "/accounts/merchant/withdrawals/42/confirm")
   1724         // Check confirm created
   1725         client.postA("/accounts/merchant/withdrawals") {
   1726             json { "amount" to "KUDOS:1" } 
   1727         }.assertOkJson<BankAccountCreateWithdrawalResponse> {
   1728             val uuid = it.withdrawal_id
   1729 
   1730             // Check err
   1731             client.postA("/accounts/merchant/withdrawals/$uuid/confirm")
   1732                 .assertConflict(TalerErrorCode.BANK_CONFIRM_INCOMPLETE)
   1733         }
   1734 
   1735         // Check confirm selected
   1736         client.postA("/accounts/merchant/withdrawals") {
   1737             json { "amount" to "KUDOS:1" } 
   1738         }.assertOkJson<BankAccountCreateWithdrawalResponse> {
   1739             val uuid = it.withdrawal_id
   1740             withdrawalSelect(uuid)
   1741 
   1742             // Check amount differs
   1743             client.postA("/accounts/merchant/withdrawals/$uuid/confirm") {
   1744                 json { "amount" to "KUDOS:2" }
   1745             }.assertConflict(TalerErrorCode.BANK_AMOUNT_DIFFERS)
   1746 
   1747             // Check OK
   1748             client.postA("/accounts/merchant/withdrawals/$uuid/confirm").assertNoContent()
   1749             // Check idempotence
   1750             client.postA("/accounts/merchant/withdrawals/$uuid/confirm").assertNoContent()
   1751 
   1752             // Check amount differs
   1753             client.postA("/accounts/merchant/withdrawals/$uuid/confirm") {
   1754                 json { "amount" to "KUDOS:2" }
   1755             }.assertConflict(TalerErrorCode.BANK_AMOUNT_DIFFERS)
   1756         }
   1757 
   1758         // Check confirm with amount
   1759         client.postA("/accounts/merchant/withdrawals") {
   1760             json {} 
   1761         }.assertOkJson<BankAccountCreateWithdrawalResponse> {
   1762             val uuid = it.withdrawal_id
   1763             withdrawalSelect(uuid)
   1764 
   1765             // Check missing amount
   1766             client.postA("/accounts/merchant/withdrawals/$uuid/confirm")
   1767                 .assertConflict(TalerErrorCode.BANK_AMOUNT_REQUIRED)
   1768 
   1769             // Check OK
   1770             client.postA("/accounts/merchant/withdrawals/$uuid/confirm") {
   1771                 json { "amount" to "KUDOS:1" } 
   1772             }.assertNoContent()
   1773             // Check idempotence
   1774             client.postA("/accounts/merchant/withdrawals/$uuid/confirm") {
   1775                 json { "amount" to "KUDOS:1" } 
   1776             }.assertNoContent()
   1777 
   1778             // Check amount differs
   1779             client.postA("/accounts/merchant/withdrawals/$uuid/confirm") {
   1780                 json { "amount" to "KUDOS:2" }
   1781             }.assertConflict(TalerErrorCode.BANK_AMOUNT_DIFFERS)
   1782         }
   1783 
   1784         // Check confirm aborted
   1785         client.postA("/accounts/merchant/withdrawals") {
   1786             json { "amount" to "KUDOS:1" } 
   1787         }.assertOkJson<BankAccountCreateWithdrawalResponse> {
   1788             val uuid = it.withdrawal_id
   1789             withdrawalSelect(uuid)
   1790             client.postA("/accounts/merchant/withdrawals/$uuid/abort").assertNoContent()
   1791 
   1792             // Check error
   1793             client.postA("/accounts/merchant/withdrawals/$uuid/confirm")
   1794                 .assertConflict(TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT)
   1795         }
   1796 
   1797         // Check balance insufficient
   1798         client.postA("/accounts/merchant/withdrawals") {
   1799             json { "amount" to "KUDOS:5" } 
   1800         }.assertOkJson<BankAccountCreateWithdrawalResponse> {
   1801             val uuid = it.withdrawal_id
   1802             withdrawalSelect(uuid)
   1803 
   1804             // Send too much money
   1805             tx("merchant", "KUDOS:5", "customer")
   1806             client.postA("/accounts/merchant/withdrawals/$uuid/confirm")
   1807                 .assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT)
   1808 
   1809             // Check can abort because not confirmed
   1810             client.postA("/accounts/merchant/withdrawals/$uuid/abort").assertNoContent()
   1811         }
   1812 
   1813         // Check confirm another user's operation
   1814         client.postA("/accounts/customer/withdrawals") {
   1815             json { "amount" to "KUDOS:1" } 
   1816         }.assertOkJson<BankAccountCreateWithdrawalResponse> {
   1817             val uuid = it.withdrawal_id
   1818             withdrawalSelect(uuid)
   1819 
   1820             // Check error
   1821             client.postA("/accounts/merchant/withdrawals/$uuid/confirm")
   1822                 .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
   1823         }
   1824 
   1825         // Check bad UUID
   1826         client.postA("/accounts/merchant/withdrawals/chocolate/confirm")
   1827             .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED)
   1828 
   1829         // Check unknown
   1830         client.postA("/accounts/merchant/withdrawals/${UUID.randomUUID()}/confirm")
   1831             .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
   1832 
   1833         // Check 2fa without body
   1834         fillTanInfo("merchant")
   1835         assertBalance("merchant", "-KUDOS:7")
   1836         client.postA("/accounts/merchant/withdrawals") {
   1837             json { "amount" to "KUDOS:1" } 
   1838         }.assertOkJson<BankAccountCreateWithdrawalResponse> {
   1839             val uuid = it.withdrawal_id
   1840             withdrawalSelect(uuid)
   1841 
   1842             client.postA("/accounts/merchant/withdrawals/$uuid/confirm")
   1843             .assertChallenge {
   1844                 assertBalance("merchant", "-KUDOS:7")
   1845             }.assertNoContent()
   1846         }
   1847 
   1848         // Check 2fa with body
   1849         fillTanInfo("merchant")
   1850         assertBalance("merchant", "-KUDOS:8")
   1851         client.postA("/accounts/merchant/withdrawals") {
   1852             json {} 
   1853         }.assertOkJson<BankAccountCreateWithdrawalResponse> {
   1854             val uuid = it.withdrawal_id
   1855             withdrawalSelect(uuid)
   1856 
   1857             client.postA("/accounts/merchant/withdrawals/$uuid/confirm") {
   1858                 json { "amount" to "KUDOS:1" }
   1859             }
   1860             .assertChallenge {
   1861                 assertBalance("merchant", "-KUDOS:8")
   1862             }.assertNoContent()
   1863         }
   1864         assertBalance("merchant", "-KUDOS:9")
   1865     }
   1866 
   1867     @Test
   1868     fun confirmWithFee() = bankSetup(conf = "test_with_fees.conf") { db ->
   1869         suspend fun run(amount: TalerAmount): HttpResponse {
   1870             val uuid = UUID.randomUUID()
   1871             // Create a selected withdrawal directly in the database to bypass checks
   1872             db.serializable("""
   1873                 INSERT INTO taler_withdrawal_operations(withdrawal_uuid,amount,selected_exchange_payto,selection_done,wallet_bank_account,creation_date)
   1874                 VALUES (?, (?, ?)::taler_amount, ?, true, 3, 0)
   1875             """) {
   1876                 bind(uuid)
   1877                 bind(amount)
   1878                 bind(exchangePayto.canonical)
   1879                 executeUpdate()
   1880             }
   1881 
   1882             return client.postA("/accounts/customer/withdrawals/$uuid/confirm")
   1883         }
   1884 
   1885         // Check insufficient fund
   1886         for (amount in listOf("KUDOS:11", "KUDOS:10", "KUDOS:0", "KUDOS:150")) {
   1887             run(TalerAmount(amount)).assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT)
   1888         }
   1889 
   1890         // Check OK
   1891         run(TalerAmount("KUDOS:9.9"))
   1892     }
   1893 }
   1894 
   1895 class CoreBankCashoutApiTest {
   1896     // POST /accounts/{USERNAME}/cashouts
   1897     @Test
   1898     fun create() = bankSetup {
   1899         authRoutine(HttpMethod.Post, "/accounts/merchant/cashouts")
   1900 
   1901         val req = obj {
   1902             "request_uid" to ShortHashCode.rand()
   1903             "amount_debit" to "KUDOS:1"
   1904             "amount_credit" to convert("KUDOS:1")
   1905         }
   1906 
   1907         // Missing info
   1908         client.postA("/accounts/customer/cashouts") {
   1909             json(req) 
   1910         }.assertConflict(TalerErrorCode.BANK_CONFIRM_INCOMPLETE)
   1911 
   1912         fillCashoutInfo("customer")
   1913 
   1914         // Check OK
   1915         val id = client.postA("/accounts/customer/cashouts") {
   1916             json(req) 
   1917         }.assertOkJson<CashoutResponse>().cashout_id
   1918 
   1919         // Check idempotent
   1920         client.postA("/accounts/customer/cashouts") {
   1921             json(req) 
   1922         }.assertOkJson<CashoutResponse> {
   1923             assertEquals(id, it.cashout_id)
   1924         }
   1925 
   1926         // Trigger conflict due to reused request_uid
   1927         client.postA("/accounts/customer/cashouts") {
   1928             json(req) {
   1929                 "amount_debit" to "KUDOS:2"
   1930                 "amount_credit" to convert("KUDOS:2")
   1931             }
   1932         }.assertConflict(TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED)
   1933 
   1934         // Check exchange account
   1935         client.postA("/accounts/exchange/cashouts") {
   1936             json(req) 
   1937         }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE)
   1938 
   1939         // Check insufficient fund
   1940         client.postA("/accounts/customer/cashouts") {
   1941             json(req) {
   1942                 "request_uid" to ShortHashCode.rand()
   1943                 "amount_debit" to "KUDOS:75"
   1944                 "amount_credit" to convert("KUDOS:75")
   1945             }
   1946         }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT)
   1947 
   1948         // Check wrong conversion
   1949         client.postA("/accounts/customer/cashouts") {
   1950             json(req) {
   1951                 "amount_credit" to convert("KUDOS:2")
   1952             }
   1953         }.assertConflict(TalerErrorCode.BANK_BAD_CONVERSION)
   1954 
   1955         // Check min amount
   1956         client.postA("/accounts/customer/cashouts") {
   1957             json(req) {
   1958                 "amount_debit" to "KUDOS:0.09"
   1959             }
   1960         }.assertConflict(TalerErrorCode.BANK_CONVERSION_AMOUNT_TO_SMALL)
   1961 
   1962         // Check custom min account
   1963         createConversionRateClass(cashout_min_amount = TalerAmount("KUDOS:10"))
   1964         client.patchAdmin("/accounts/customer") {
   1965             json {
   1966                 "conversion_rate_class_id" to 1
   1967             }
   1968         }.assertNoContent()
   1969         client.postA("/accounts/customer/cashouts") {
   1970             json(req) {
   1971                 "amount_debit" to "KUDOS:5"
   1972                 "amount_credit" to convert("KUDOS:5")
   1973             }
   1974         }.assertConflict(TalerErrorCode.BANK_CONVERSION_AMOUNT_TO_SMALL)
   1975         client.patchAdmin("/accounts/customer") {
   1976             json {
   1977                 "conversion_rate_class_id" to (null as Long?)
   1978             }
   1979         }.assertNoContent()
   1980 
   1981         // Check wrong currency
   1982         client.postA("/accounts/customer/cashouts") {
   1983             json(req) {
   1984                 "amount_debit" to "EUR:1"
   1985             }
   1986         }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH)
   1987         client.postA("/accounts/customer/cashouts") {
   1988             json(req) {
   1989                 "amount_credit" to "KUDOS:1"
   1990             } 
   1991         }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH)
   1992 
   1993         // Check 2fa
   1994         fillTanInfo("customer")
   1995         assertBalance("customer", "-KUDOS:1")
   1996         client.postA("/accounts/customer/cashouts") {
   1997             json(req) {
   1998                 "request_uid" to ShortHashCode.rand()
   1999             }
   2000         }.assertChallenge {
   2001             assertBalance("customer", "-KUDOS:1")
   2002         }.assertOkJson<CashoutResponse> {
   2003             assertBalance("customer", "-KUDOS:2")
   2004         }
   2005     }
   2006 
   2007     // GET /accounts/{USERNAME}/cashouts/{CASHOUT_ID}
   2008     @Test
   2009     fun get() = bankSetup {
   2010         authRoutine(HttpMethod.Get, "/accounts/merchant/cashouts/42", allowAdmin = true)
   2011         fillCashoutInfo("customer")
   2012 
   2013         val amountDebit = TalerAmount("KUDOS:1.5")
   2014         val amountCredit = convert("KUDOS:1.5")
   2015         val req = obj {
   2016             "amount_debit" to amountDebit
   2017             "amount_credit" to amountCredit
   2018         }
   2019 
   2020         // Check confirm
   2021         client.postA("/accounts/customer/cashouts") {
   2022             json(req) { "request_uid" to ShortHashCode.rand() }
   2023         }.assertOkJson<CashoutResponse> {
   2024             val id = it.cashout_id
   2025             client.getA("/accounts/customer/cashouts/$id")
   2026                 .assertOkJson<CashoutStatusResponse> {
   2027                 assertEquals(amountDebit, it.amount_debit)
   2028                 assertEquals(amountCredit, it.amount_credit)
   2029             }
   2030         }
   2031 
   2032         // Check bad UUID
   2033         client.getA("/accounts/customer/cashouts/chocolate")
   2034             .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED)
   2035 
   2036         // Check unknown
   2037         client.getA("/accounts/customer/cashouts/42")
   2038             .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
   2039 
   2040         // Check get another user's operation
   2041         client.postA("/accounts/customer/cashouts") {
   2042             json(req) { "request_uid" to ShortHashCode.rand() }
   2043         }.assertOkJson<CashoutResponse> {
   2044             val id = it.cashout_id
   2045 
   2046             // Check error
   2047             client.getA("/accounts/merchant/cashouts/$id")
   2048                 .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
   2049         }
   2050     }
   2051 
   2052     // GET /accounts/{USERNAME}/cashouts
   2053     @Test
   2054     fun history() = bankSetup {
   2055         authRoutine(HttpMethod.Get, "/accounts/merchant/cashouts", allowAdmin = true)
   2056         historyRoutine<Cashouts>(
   2057             url = "/accounts/customer/cashouts",
   2058             ids = { it.cashouts.map { it.cashout_id } },
   2059             registered = listOf { cashout("KUDOS:0.1") },
   2060             polling = false
   2061         )
   2062     }
   2063 
   2064     // GET /cashouts
   2065     @Test
   2066     fun globalHistory() = bankSetup {
   2067         authRoutine(HttpMethod.Get, "/cashouts", requireAdmin = true)
   2068         historyRoutine<GlobalCashouts>(
   2069             url = "/cashouts",
   2070             ids = { it.cashouts.map { it.cashout_id } },
   2071             registered = listOf { cashout("KUDOS:0.1") },
   2072             polling = false,
   2073             auth = "admin"
   2074         )
   2075     }
   2076 
   2077     @Test
   2078     fun notImplemented() = bankSetup("test_no_conversion.conf") {
   2079         client.get("/accounts/customer/cashouts")
   2080             .assertNotImplemented()
   2081     }
   2082 }
   2083 
   2084 class CoreBankTanApiTest {
   2085     // POST /accounts/{USERNAME}/challenge/{challenge_id}
   2086     @Test
   2087     fun send() = bankSetup {
   2088         suspend fun HttpResponse.expectMfa(vararg tans: Pair<TanChannel, String>): HttpResponse {
   2089             return assertChallenge { res ->
   2090                 assertEquals(setOf(*tans), res.challenges.map { it.tan_channel to it.tan_info }.toSet())
   2091                 assertFalse(res.combi_and)
   2092             }
   2093         }
   2094         suspend fun HttpResponse.expectValidation(vararg tans: Pair<TanChannel, String>): HttpResponse {
   2095             return assertChallenge { res ->
   2096                 assertEquals(setOf(*tans), res.challenges.map { it.tan_channel to it.tan_info }.toSet())
   2097                 assertTrue(res.combi_and)
   2098             }
   2099         }
   2100 
   2101         // Set up 2fa 
   2102         client.patchA("/accounts/merchant") {
   2103             json { 
   2104                 "contact_data" to obj {
   2105                     "phone" to "+99"
   2106                     "email" to "email@example.com"
   2107                 }
   2108                 "tan_channel" to "sms"
   2109             }
   2110         }.expectValidation(TanChannel.sms to "+99")
   2111             .assertNoContent()
   2112         
   2113         // Update 2fa settings - first 2FA challenge then new tan channel check
   2114         client.patchA("/accounts/merchant") {
   2115             json { // Info change
   2116                 "contact_data" to obj { "phone" to "+98" }
   2117             }
   2118         }.expectValidation(TanChannel.sms to "+99", TanChannel.sms to "+98")
   2119             .assertNoContent()
   2120         client.patchA("/accounts/merchant") {
   2121             json { // Channel change
   2122                 "tan_channel" to "email"
   2123             }
   2124         }.expectValidation(TanChannel.sms to "+98", TanChannel.email to "email@example.com")
   2125             .assertNoContent()
   2126         client.patchA("/accounts/merchant") {
   2127             json { // Both change
   2128                 "contact_data" to obj { "phone" to "+97" }
   2129                 "tan_channel" to "sms"
   2130             }
   2131         }.expectValidation(TanChannel.email to "email@example.com", TanChannel.sms to "+97")
   2132             .assertNoContent()
   2133 
   2134         // Disable 2fa
   2135         client.patchA("/accounts/merchant") {
   2136             json { "tan_channel" to null as String? }
   2137         }.expectValidation(TanChannel.sms to "+97")
   2138             .assertNoContent()
   2139 
   2140         // Update mfa settings - first mfa challenge then new tan channel check
   2141         client.patchA("/accounts/merchant") {
   2142             json { // All channels
   2143                 "tan_channels" to setOf("sms", "email")
   2144             }
   2145         }.expectValidation(TanChannel.sms to "+97", TanChannel.email to "email@example.com")
   2146             .assertNoContent()
   2147         client.patchA("/accounts/merchant") {
   2148             json { // All info changes
   2149                 "contact_data" to obj {
   2150                     "phone" to "+99"
   2151                     "email" to "email2@example.com"
   2152                 }
   2153             }
   2154         }.expectMfa(TanChannel.sms to "+97", TanChannel.email to "email@example.com")
   2155             .expectValidation(TanChannel.sms to "+99", TanChannel.email to "email2@example.com")
   2156             .assertNoContent()
   2157 
   2158         // Disable mfa
   2159         client.patchA("/accounts/merchant") {
   2160             json { "tan_channels" to emptySet<String>() }
   2161         }.expectMfa(TanChannel.sms to "+99", TanChannel.email to "email2@example.com")
   2162             .assertNoContent()
   2163         
   2164 
   2165         // Admin has no 2FA
   2166         client.patchAdmin("/accounts/merchant") {
   2167             json { 
   2168                 "contact_data" to obj { "phone" to "+99" }
   2169                 "tan_channel" to "sms"
   2170             }
   2171         }.assertNoContent()
   2172         client.patchAdmin("/accounts/merchant") {
   2173             json { "tan_channel" to "email" }
   2174         }.assertNoContent()
   2175         client.patchAdmin("/accounts/merchant") {
   2176             json { "tan_channel" to null as String? }
   2177         }.assertNoContent()
   2178 
   2179         // Check retry and invalidate
   2180         client.patchA("/accounts/merchant") {
   2181             json { 
   2182                 "contact_data" to obj { "phone" to "+88" }
   2183                 "tan_channel" to "sms"
   2184             }
   2185         }.assertChallenge().assertNoContent()
   2186         client.patchA("/accounts/merchant") {
   2187             json { "is_public" to false }
   2188         }.assertAcceptedJson<ChallengeResponse> {
   2189             val challenge = it.challenges[0]
   2190             // Check ok
   2191             client.postA("/accounts/merchant/challenge/${challenge.challenge_id}")
   2192                 .assertOk()
   2193             val code = tanCode("+88")
   2194             assertNotNull(code)
   2195             // Check retry
   2196             client.postA("/accounts/merchant/challenge/${challenge.challenge_id}")
   2197                 .assertOk()
   2198             assertNull(tanCode("+88"))
   2199             // Idempotent patch does nothing
   2200             client.patchA("/accounts/merchant") {
   2201                 json { 
   2202                     "contact_data" to obj { "phone" to "+88" }
   2203                     "tan_channel" to "sms"
   2204                 }
   2205             }
   2206             client.postA("/accounts/merchant/challenge/${challenge.challenge_id}")
   2207                 .assertOk()
   2208             assertNull(tanCode("+88"))
   2209 
   2210             // Change 2fa settings
   2211             client.patchA("/accounts/merchant") {
   2212                 json { 
   2213                     "tan_channel" to "email"
   2214                 }
   2215             }.expectValidation(TanChannel.sms to "+88", TanChannel.email to "email2@example.com")
   2216                 .assertNoContent()
   2217 
   2218             // Check invalidated
   2219             client.postA("/accounts/merchant/challenge/${challenge.challenge_id}/confirm") {
   2220                 json { "tan" to code }
   2221             }.assertNotFound(TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED)
   2222             client.patchA("/accounts/merchant") {
   2223                 headers[TALER_CHALLENGE_IDS] = "${challenge.challenge_id}"
   2224                 json { "is_public" to false }
   2225             }.expectValidation(TanChannel.email to "email2@example.com")
   2226                 .assertNoContent()
   2227         }
   2228 
   2229         // Unknown challenge
   2230         client.postA("/accounts/merchant/challenge/${UUID.randomUUID()}")
   2231             .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
   2232     }
   2233 
   2234     @Test
   2235     fun sendRateLimited() = bankSetup {
   2236         fillTanInfo("merchant")
   2237 
   2238         suspend fun ApplicationTestBuilder.txChallenge() 
   2239             = client.postA("/accounts/merchant/transactions") {
   2240                 json {
   2241                     "payto_uri" to "$customerPayto?message=tx&amount=KUDOS:0.1"
   2242                 }
   2243             }.assertAcceptedJson<ChallengeResponse>().challenges[0]
   2244         suspend fun ApplicationTestBuilder.submit(challenge: Challenge)
   2245             = client.postA("/accounts/merchant/challenge/${challenge.challenge_id}")
   2246                 .assertOkJson<ChallengeRequestResponse>()
   2247             
   2248 
   2249         // Start a legitimate challenge and submit it
   2250         val oldChallenge = txChallenge()
   2251         submit(oldChallenge)
   2252         val tanCode = tanCode(oldChallenge.tan_info)
   2253 
   2254         // Challenge creation is not rate limited
   2255         repeat(MAX_ACTIVE_CHALLENGES*2) {
   2256             txChallenge()
   2257         }
   2258 
   2259         // Challenge submission is rate limited
   2260         repeat(MAX_ACTIVE_CHALLENGES-1) {
   2261             submit(txChallenge())
   2262         }
   2263         val challenge = txChallenge()
   2264         client.postA("/accounts/merchant/challenge/${challenge.challenge_id}")
   2265             .assertTooManyRequests(TalerErrorCode.BANK_TAN_RATE_LIMITED)
   2266 
   2267         // Old already submitted challenge still works
   2268         val transmission = submit(oldChallenge)
   2269         client.postA("/accounts/merchant/challenge/${oldChallenge.challenge_id}/confirm") {
   2270             json { "tan" to tanCode }
   2271         }.assertNoContent()
   2272 
   2273         // Now an active challenge slot have been freed
   2274         submit(challenge)
   2275 
   2276         // We are rate limited again
   2277         val newChallenge = txChallenge()
   2278         client.postA("/accounts/merchant/challenge/${newChallenge.challenge_id}")
   2279             .assertTooManyRequests(TalerErrorCode.BANK_TAN_RATE_LIMITED)
   2280     }
   2281 
   2282     // POST /accounts/{USERNAME}/challenge/{challenge_id}
   2283     @Test
   2284     fun sendTanErr() = bankSetup("test_tan_err.conf") {
   2285         // Check fail
   2286         fillTanInfo("merchant")
   2287         client.patchA("/accounts/merchant") {
   2288             json { "is_public" to false }
   2289         }.assertAcceptedJson<ChallengeResponse> {
   2290             val challenge = it.challenges[0]
   2291             client.postA("/accounts/merchant/challenge/${challenge.challenge_id}")
   2292                 .assertStatus(HttpStatusCode.BadGateway, TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED)
   2293         }
   2294     }
   2295 
   2296     // POST /accounts/{USERNAME}/challenge/{challenge_id}/confirm
   2297     @Test
   2298     fun confirm() = bankSetup {
   2299         fillTanInfo("merchant")
   2300 
   2301         // Check simple case
   2302         client.patchA("/accounts/merchant") {
   2303             json { "is_public" to false }
   2304         }.assertAcceptedJson<ChallengeResponse> {
   2305             val challenge = it.challenges[0]
   2306             val id = challenge.challenge_id
   2307             client.postA("/accounts/merchant/challenge/$id")
   2308                 .assertOkJson<ChallengeRequestResponse>()
   2309             val code = tanCode(challenge.tan_info)
   2310 
   2311             // Check bad TAN code
   2312             client.postA("/accounts/merchant/challenge/$id/confirm") {
   2313                 json { "tan" to "nice-try" } 
   2314             }.assertConflict(TalerErrorCode.BANK_TAN_CHALLENGE_FAILED)
   2315 
   2316             // Check wrong account
   2317             client.postA("/accounts/customer/challenge/$id/confirm") {
   2318                 json { "tan" to "nice-try" } 
   2319             }.assertConflict(TalerErrorCode.BANK_TAN_CHALLENGE_FAILED)
   2320         
   2321             // Check OK
   2322             client.postA("/accounts/merchant/challenge/$id/confirm") {
   2323                 json { "tan" to code }
   2324             }.assertNoContent()
   2325             // Check idempotence
   2326             client.postA("/accounts/merchant/challenge/$id/confirm") {
   2327                 json { "tan" to code }
   2328             }.assertNoContent()
   2329 
   2330             // Unknown challenge
   2331             client.postA("/accounts/merchant/challenge/${UUID.randomUUID()}/confirm") {
   2332                 json { "tan" to code }
   2333             }.assertNotFound(TalerErrorCode.BANK_CHALLENGE_NOT_FOUND)
   2334         }
   2335         
   2336         // Check invalidation
   2337         client.patchA("/accounts/merchant") {
   2338             json { "is_public" to true }
   2339         }.assertAcceptedJson<ChallengeResponse> {
   2340             val challenge = it.challenges[0]
   2341             val id = challenge.challenge_id
   2342             client.postA("/accounts/merchant/challenge/$id")
   2343                 .assertOkJson<ChallengeRequestResponse>()
   2344              
   2345             // Check invalidated
   2346             fillTanInfo("merchant")
   2347             client.postA("/accounts/merchant/challenge/$id/confirm") {
   2348                 json { "tan" to tanCode(challenge.tan_info) }
   2349             }.assertNotFound(TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED)
   2350 
   2351             client.postA("/accounts/merchant/challenge/$id")
   2352                 .assertNotFound(TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED)
   2353         }
   2354     }
   2355 }
   2356 
   2357 class CoreBankConversionApiTest {
   2358     // POST /conversion-rate-classes
   2359     // GET /conversion-rate-classes
   2360     // GET /conversion-rate-classes/{CLASS_ID}
   2361     @Test
   2362     fun classes() = bankSetup() {
   2363         authRoutine(HttpMethod.Post, "/conversion-rate-classes", requireAdmin = true)
   2364         authRoutine(HttpMethod.Get, "/conversion-rate-classes", requireAdmin = true)
   2365         authRoutine(HttpMethod.Get, "/conversion-rate-classes/1", requireAdmin = true)
   2366 
   2367         val fullInput = obj {
   2368             "description" to "A nice little class"
   2369             "cashin_ratio" to "0.1"
   2370             "cashin_fee" to "KUDOS:0.2"
   2371             "cashin_tiny_amount" to "KUDOS:0.3"
   2372             "cashin_rounding_mode" to "nearest"
   2373             "cashin_min_amount" to "EUR:0"
   2374             "cashout_ratio" to "0.4"
   2375             "cashout_fee" to "EUR:0.5"
   2376             "cashout_tiny_amount" to "EUR:0.6"
   2377             "cashout_rounding_mode" to "zero"
   2378             "cashout_min_amount" to "KUDOS:0.7"
   2379         }
   2380 
   2381         // Check no classes
   2382         client.getAdmin("/conversion-rate-classes").assertNoContent()
   2383         client.getAdmin("/conversion-rate-classes/1").assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
   2384         client.patchAdmin("/conversion-rate-classes/1") {
   2385             json(fullInput) {
   2386                 "name" to "Class"
   2387             }
   2388         }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
   2389         client.deleteAdmin("/conversion-rate-classes/1").assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
   2390 
   2391         // Create full
   2392         val full = client.postAdmin("/conversion-rate-classes") {
   2393             json(fullInput) {
   2394                 "name" to "Class n°1"
   2395             }
   2396         }.assertOkJson<ConversionRateClassResponse> { 
   2397             assertEquals(it.conversion_rate_class_id, 1)
   2398             val rate = client.getAdmin("/conversion-rate-classes/${it.conversion_rate_class_id}").assertOkJson<ConversionRateClass>()
   2399             client.patchAdmin("/conversion-rate-classes/${it.conversion_rate_class_id}") {
   2400                 json {
   2401                     "name" to "Class n°1"
   2402                 }
   2403             }.assertNoContent()
   2404             it.conversion_rate_class_id
   2405         }
   2406         // Create empty
   2407         val empty = client.postAdmin("/conversion-rate-classes") {
   2408             json {
   2409                 "name" to "Class n°2"
   2410             }
   2411         }.assertOkJson<ConversionRateClassResponse> { 
   2412             assertEquals(it.conversion_rate_class_id, 2)
   2413             val rate = client.getAdmin("/conversion-rate-classes/${it.conversion_rate_class_id}").assertOkJson<ConversionRateClass>()
   2414             client.patchAdmin("/conversion-rate-classes/${it.conversion_rate_class_id}") {
   2415                 json(fullInput) {
   2416                     "name" to "Class n°2"
   2417                 }
   2418             }.assertNoContent()
   2419             it.conversion_rate_class_id
   2420         }
   2421 
   2422         // Bad currency
   2423         client.postAdmin("/conversion-rate-classes") {
   2424             json(fullInput) {
   2425                 "name" to "Bad currency"
   2426                 "cashout_fee" to "CHF:0.003"
   2427             }
   2428         }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH)
   2429 
   2430         // Name reuse currency
   2431         client.postAdmin("/conversion-rate-classes") {
   2432             json(fullInput) {
   2433                 "name" to "Class n°1"
   2434             }
   2435         }.assertConflict(TalerErrorCode.BANK_NAME_REUSE)
   2436         client.patchAdmin("/conversion-rate-classes/2") {
   2437             json(fullInput) {
   2438                 "name" to "Class n°1"
   2439             }
   2440         }.assertConflict(TalerErrorCode.BANK_NAME_REUSE)
   2441          client.patchAdmin("/conversion-rate-classes/1") {
   2442             json(fullInput) {
   2443                 "name" to "Class n°1"
   2444             }
   2445         }.assertNoContent()
   2446 
   2447         // Page
   2448         client.getAdmin("/conversion-rate-classes").assertOkJson<ConversionRateClasses> {
   2449             assertEquals(it.classes.size, 2)
   2450         }
   2451         val generated = (0 until 5).map { createConversionRateClass() }
   2452         client.getAdmin("/conversion-rate-classes").assertOkJson<ConversionRateClasses> {
   2453             assertEquals(it.classes.size, 7)
   2454         }
   2455         client.getAdmin("/conversion-rate-classes?filter_name=Gen").assertOkJson<ConversionRateClasses> {
   2456             assertEquals(it.classes.size, 5)
   2457         }
   2458 
   2459         // Delete all
   2460         for (id in listOf(full.conversion_rate_class_id, empty.conversion_rate_class_id) + generated) {
   2461             client.deleteAdmin("/conversion-rate-classes/$id").assertNoContent()
   2462             client.deleteAdmin("/conversion-rate-classes/$id").assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
   2463         }
   2464         client.getAdmin("/conversion-rate-classes").assertNoContent()
   2465     }
   2466 
   2467     @Test
   2468     fun notImplemented() = bankSetup("test_no_conversion.conf") {
   2469         client.getAdmin("conversion-rate-classes/1").assertNotImplemented()
   2470         client.getAdmin("conversion-rate-classes").assertNotImplemented()
   2471     }
   2472 }