libeufin

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

SubjectTest.kt (8061B)


      1 /*
      2  * This file is part of LibEuFin.
      3  * Copyright (C) 2024, 2025, 2026 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 tech.libeufin.common.*
     21 import kotlin.test.Test
     22 import kotlin.test.assertEquals
     23 import kotlin.test.assertFails
     24 
     25 class SubjectTest {
     26     fun assertFailsMsg(msg: String, lambda: () -> Unit) {
     27         val failure = assertFails(lambda)
     28         assertEquals(msg, failure.message)
     29     }
     30 
     31     @Test
     32     fun parseIncoming() {
     33         val key = "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0";
     34         val other = "00Q979QSMJ29S7BJT3DDAVC5A0DR5Z05B7N0QT1RCBQ8FXJPZ6RG";
     35 
     36         for (ty in sequenceOf(IncomingType.reserve, IncomingType.kyc, IncomingType.map)) {
     37             val prefix = when (ty) {
     38                 IncomingType.reserve -> ""
     39                 IncomingType.kyc -> "KYC"
     40                 IncomingType.map -> "MAP"
     41             }
     42             val standard = "$prefix$key"
     43             val (standardL, standardR) = standard.chunked(standard.length / 2 + 1)
     44             val mixed = "${prefix}4mzt6RS3rvb3b0e2rdmyw0yra3y0vphyv0cyde6xbb0ympfxceg0"
     45             val (mixedL, mixedR) = mixed.chunked(mixed.length / 2 + 1)
     46             val other_standard = "$prefix$other"
     47             val other_mixed = "${prefix}TEGY6d9mh9pgwvwpgs0z0095z854xegfy7jj202yd0esp8p0za60"
     48             val key = when (ty) {
     49                 IncomingType.reserve -> IncomingSubject.Reserve(EddsaPublicKey(key))
     50                 IncomingType.kyc -> IncomingSubject.Kyc(EddsaPublicKey(key))
     51                 IncomingType.map -> IncomingSubject.Map(EddsaPublicKey(key))
     52             }
     53 
     54             // Check succeed if standard or mixed
     55             for (case in sequenceOf(standard, mixed)) {
     56                 for (test in sequenceOf(
     57                     "noise $case noise",
     58                     "$case noise to the right",
     59                     "noise to the left $case",
     60                     "    $case     ",
     61                     "noise\n$case\nnoise",
     62                     "Test+$case"
     63                 )) {
     64                     assertEquals(key, parseIncomingSubject(test))
     65                 }
     66             }
     67 
     68             // Check succeed if standard or mixed and split
     69             for ((L, R) in sequenceOf(standardL to standardR, mixedL to mixedR)) {
     70                 for (case in sequenceOf(
     71                     "left $L$R right",
     72                     "left $L $R right",
     73                     "left $L-$R right",
     74                     "left $L+$R right",
     75                     "left $L\n$R right",
     76                     "left $L%20$R right",
     77                     "left $L-+\n$R right",
     78                     "left $L - $R right",
     79                     "left $L + $R right",
     80                     "left $L \n $R right",
     81                     "left $L - + \n $R right",
     82                 )) {
     83                     assertEquals(key, parseIncomingSubject(case))
     84                 }
     85             }
     86 
     87             // Check concat parts
     88             for (chunkSize in 1 until standard.length) {
     89                 val chunked = standard.chunked(chunkSize).joinToString(" ")
     90                 for (case in sequenceOf(chunked, "left ${chunked} right")) {
     91                     assertEquals(key, parseIncomingSubject(case))
     92                 }
     93             }
     94 
     95             // Check failed when multiple key
     96             for (case in sequenceOf(
     97                 "$standard $other_standard",
     98                 "$mixed $other_mixed",
     99             )) {
    100                 assertFailsMsg("found multiple reserve public key") {
    101                     parseIncomingSubject(case)
    102                 }
    103             }
    104 
    105             // Check accept redundant key
    106             for (case in sequenceOf(
    107                 "$standard $standard $mixed $mixed",   // Accept redundant key
    108                 "$mixedL-$mixedR $standardL-$standardR",
    109                 "$standard $other_mixed",              // Prefer high quality
    110             )) {
    111                 assertEquals(key, parseIncomingSubject(case))
    112             }
    113 
    114             // Check failure if malformed or missing
    115             for (case in sequenceOf(
    116                 "does not contain any reserve",                        // Check fail if none
    117                 standard.substring(1),                    // Check fail if missing char
    118                 "2MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0" // Check fail if not a valid key
    119             )) {
    120                 assertFailsMsg("missing reserve public key") {
    121                     parseIncomingSubject(case)
    122                 }
    123             }
    124 
    125             if (ty == IncomingType.kyc) {
    126                 // Prefer prefixed over unprefixed
    127                 for (case in sequenceOf(
    128                     "$other $standard", "$other $mixed"
    129                 )) {
    130                     assertEquals(key, parseIncomingSubject(case))
    131                 }
    132             }
    133         }
    134 
    135         // Admin balance adjust
    136         for (subject in sequenceOf(
    137             "ADMIN BALANCE ADJUST",
    138             "ADMIN:BALANCE:ADJUST",
    139             "AdminBalanceAdjust",
    140             "ignore aDmIn:BaLaNCe AdJUsT"
    141         )) {
    142             assertEquals(
    143                 IncomingSubject.AdminBalanceAdjust,
    144                 parseIncomingSubject(subject)
    145             )
    146         }
    147     }
    148 
    149     /** Test parsing logic using real use case */
    150     @Test
    151     fun realIncoming() {
    152         // Good reserve cases
    153         for ((subject, key) in sequenceOf(
    154             "Taler TEGY6d9mh9pgwvwpgs0z0095z854xegfy7j j202yd0esp8p0za60" to "TEGY6d9mh9pgwvwpgs0z0095z854xegfy7jj202yd0esp8p0za60",
    155             "00Q979QSMJ29S7BJT3DDAVC5A0DR5Z05B7N 0QT1RCBQ8FXJPZ6RG" to "00Q979QSMJ29S7BJT3DDAVC5A0DR5Z05B7N0QT1RCBQ8FXJPZ6RG",
    156             "Taler NDDCAM9XN4HJZFTBD8V6FNE2FJE8G Y734PJ5AGQMY06C8D4HB3Z0" to "NDDCAM9XN4HJZFTBD8V6FNE2FJE8GY734PJ5AGQMY06C8D4HB3Z0",
    157             "KYCVEEXTBXBEMCS5R64C24GFNQVWBN5R2F9QSQ7PN8QXAP1NG4NG" to "KYCVEEXTBXBEMCS5R64C24GFNQVWBN5R2F9QSQ7PN8QXAP1NG4NG",
    158             "Taler%20NDDCAM9XN4HJZFTBD8V6FNE2FJE8G Y734PJ5AGQMY06C8D4HB3Z0" to "NDDCAM9XN4HJZFTBD8V6FNE2FJE8GY734PJ5AGQMY06C8D4HB3Z0",
    159         )) {
    160             assertEquals(
    161                 IncomingSubject.Reserve(EddsaPublicKey(key)),
    162                 parseIncomingSubject(subject)
    163             )
    164         }
    165         // Good kyc cases
    166         for ((subject, key) in sequenceOf(
    167             "KYC JW398X85FWPKKMS0EYB6TQ1799RMY5DDXTZ FPW4YC3WJ2DWSJT70" to "JW398X85FWPKKMS0EYB6TQ1799RMY5DDXTZFPW4YC3WJ2DWSJT70"
    168         )) {
    169             assertEquals(
    170                 IncomingSubject.Kyc(EddsaPublicKey(key)),
    171                 parseIncomingSubject(subject)
    172             )
    173         }
    174     }
    175 
    176     @Test
    177     fun outgoing() {
    178         val key = ShortHashCode.rand()
    179 
    180         run {
    181             // Without metadata
    182             val subject = "$key http://exchange.example.com/"
    183             val parsed = parseOutgoingSubject(subject)
    184             assertEquals(parsed, Triple(key, BaseURL.parse("http://exchange.example.com/"), null))
    185             assertEquals(subject, fmtOutgoingSubject(parsed.first, parsed.second, parsed.third))
    186         }
    187 
    188         run {
    189             // With metadata
    190             val subject =
    191                 "Accounting:id.42 $key http://exchange.example.com/"
    192             val parsed = parseOutgoingSubject(subject)
    193             assertEquals(
    194                 parsed,
    195                 Triple(key, BaseURL.parse("http://exchange.example.com/"), "Accounting:id.42")
    196             )
    197             assertEquals(subject, fmtOutgoingSubject(parsed.first, parsed.second, parsed.third))
    198         }
    199     }
    200 }