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 }