test_contract.c (20547B)
1 /* 2 This file is part of TALER 3 (C) 2015, 2021 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 13 You should have received a copy of the GNU General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 /** 18 * @file src/util/test_contract.c 19 * @brief Tests for contract parsing/serializing 20 * @author Iván Ávalos (ivan@avalos.me) 21 */ 22 23 #include "platform.h" 24 #include <gnunet/gnunet_util_lib.h> 25 #include <gnunet/gnunet_common.h> 26 #include <gnunet/gnunet_json_lib.h> 27 #include "taler/taler_merchant_util.h" 28 #include <jansson.h> 29 30 const char *contract_common = "{\n" 31 " \"summary\": \"Test order\",\n" 32 " \"summary_i18n\": {\n" 33 " \"en\": \"Test order\",\n" 34 " \"es\": \"Orden de prueba\"\n" 35 " },\n" 36 " \"order_id\": \"test\",\n" 37 " \"public_reorder_url\": \"https://test.org/reorder\",\n" 38 " \"fulfillment_url\": \"https://test.org/fulfillment\",\n" 39 " \"fulfillment_message\": \"Thank you for your purchase!\",\n" 40 " \"fulfillment_message_i18n\": {\n" 41 " \"en\": \"Thank you for your purchase!\",\n" 42 " \"es\": \"¡Gracias por su compra!\"\n" 43 " },\n" 44 " \"products\": [],\n" 45 " \"timestamp\": {\"t_s\": 1736174497},\n" 46 " \"refund_deadline\": {\"t_s\": 1736174497},\n" 47 " \"pay_deadline\": {\"t_s\": 1736174497},\n" 48 " \"wire_transfer_deadline\": {\"t_s\": 1736174497},\n" 49 " \"merchant_pub\": \"F80MFRG8HVH6R9CQ47KRFQSJP3T6DBJ4K1D9B703RJY3Z39TBMJ0\",\n" 50 " \"merchant_base_url\": \"https://test.org/merchant\",\n" 51 " \"merchant\": {\n" 52 " \"name\": \"Test merchant\",\n" 53 " \"email\": \"test@test.org\",\n" 54 " \"website\": \"https://test.org/merchant\",\n" 55 " \"logo\": \"data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAADRUlEQVRYhe2WXUgUURiG39l1Vt021C2piJAQyhXEXVNKysoSd8nywgLNn1CIMCLposCLxOii8KasC2+CFB1WRVASMpMUKiyIaLXyBwmhoh9Kzc1W929murAZ9+znZl2EXey5OvOc93zfO+ec+eZwZmuLjFVsmtVMHjYQNvBfGIhYSSAD6LcXYJ1RrzLO1AhzAg9oNHD0FGPBLSJ2bwuSjRE4U2rCydIdmJpxIae4C9wK8blQdUCWgaH7pepzZXUf3rx34eqFXcgwbwIANLUP40bjKzzuLIRBzwMATFY7ojgJL3pL1eRmqwAuhJNlDSiTTQebEcUv7pKjd9GMxSaouj9hHp+M0f4yeH0Sdh6xEwPkDOTu2QzuV4CVkltsAkYnptTxQI3CInkOFpsAHa9Byjbj7w2Ikoy6i9lMIh+nDfmWAFBS1UuYonX6OIY13zwEOWi9GQMv+8pwS3Awgtf3jjPJg5ME9wPZZH8Jw7r7JphzRQwAQIMwovZHZ/wkcF11Jj5/cRHumvfjVFES4Z/mRLVfe+0ZGVcNLHglMjg3eALff3gZlrs/ETnFnUSbdtiO0+XpDHN7RIz1FBKtKC3tg2rAtDUG35xuRqjjNdhd2EUC8BH0m9LraE2rrO5DXEwU4ZJ2qfyos6adXqw1RBKx0bBirQrZJj/MEyaKQKxeu4yBWTcitOybuT0i7t7OJ0G8flq7PD7KepryMb/AniOtFph1eqgB3TLLGpPVAsManmF3escx0HqUaB92HENH9wjDDHoeG7PpFxK4hWTjqspT1H6ykS7/pfrn2BCvJzzeGI0rDQ7CE2OXYtScTSfjjIFUq4CKolRGkJrXxhQZpQWyUOPbc9nSW5CXBLOVXRHGgIYDzl8eYAJqRD9JohSe+toswgK1eo3EsPJzPeSnRLag/8lHdYJy2ILru8L2ZSao/4Tg5ApzeaTFWD4Jw+MzwemWvw9YbAJkGRh5UAZg8ddssQmIitapCeZcXlhsAmqq0lX26Ok7bDlgR17Gesas2SaEvBeEvA8oTZaBgbYCGOP0aG534HrjCIam/XAPliFSp0VaXitkUcTQWx/ksQp13tdpF3JLaBH7awP/uq36nTBsIGxg1Q38BGj1Qe2GKmj3AAAAAElFTkSuQmCC\",\n" 56 " \"address\": {},\n" // TODO 57 " \"jurisdiction\": {}\n" // TODO 58 " },\n" 59 " \"h_wire\": \"WYEMPXPRA87Y1QBJAJSR5SMB8V3QN2MMHAKDAEN04XC8TQ6TBEJF5KAKT3Y8KKP9W0TW3A2PH1YB22EZJ9TA7HX1P5BFTJQ660GS1TG\",\n" 60 " \"wire_method\": \"iban\",\n" 61 " \"exchanges\": [],\n" 62 " \"delivery_location\": {},\n" // TODO 63 " \"delivery_date\": {\"t_s\": 1736174497},\n" 64 " \"nonce\": \"test-nonce\",\n" 65 " \"auto_refund\": {\"d_us\": \"forever\"},\n" 66 " \"extra\": {\n" 67 " \"key0\": \"value0\",\n" 68 " \"key1\": \"value1\",\n" 69 " \"key2\": 2\n" 70 " },\n" 71 " \"minimum_age\": 100\n" 72 "}"; 73 74 const char *contract_v0 = "{\n" 75 " \"version\": 0,\n" 76 " \"amount\": \"KUDOS:10\",\n" 77 " \"amount_external\": [\n" 78 " {\n" 79 " \"method\": \"cash\",\n" 80 " \"id\": \"cash1\",\n" 81 " \"amount\": \"KUDOS:30\",\n" 82 " \"cashier_number\": \"7\"\n" 83 " },\n" 84 " {\n" 85 " \"method\": \"card\",\n" 86 " \"id\": \"sumup1\",\n" 87 " \"amount\": \"KUDOS:5\",\n" 88 " \"terminal_id\": 42,\n" 89 " \"contactless\": true\n" 90 " }\n" 91 " ],\n" 92 " \"max_fee\": \"KUDOS:0.2\"\n" 93 "}"; 94 95 const char *contract_v1 = "{\n" 96 " \"version\": 1,\n" 97 " \"amount_external\": [\n" 98 " {\n" 99 " \"method\": \"cash\",\n" 100 " \"id\": \"cash1\",\n" 101 " \"amount\": \"KUDOS:3\"\n" 102 " }\n" 103 " ],\n" 104 " \"choices\": [\n" 105 " {\n" 106 " \"amount\": \"KUDOS:1\",\n" 107 " \"max_fee\": \"KUDOS:0.5\",\n" 108 " \"description\": \"Subscription in, discount out\",\n" 109 " \"description_i18n\": {\n" 110 " \"en\": \"This is a test choice\",\n" 111 " \"es\": \"Esta es una opción de prueba\"\n" 112 " },\n" 113 " \"inputs\": [\n" 114 " {\n" 115 " \"type\": \"token\",\n" 116 " \"token_family_slug\": \"test-subscription\",\n" 117 " \"count\": 1\n" 118 " }\n" 119 " ],\n" 120 " \"outputs\": [\n" 121 " {\n" 122 " \"type\": \"token\",\n" 123 " \"token_family_slug\": \"test-discount\",\n" 124 " \"count\": 2,\n" 125 " \"key_index\": 0\n" // TODO 126 " },\n" 127 " {\n" 128 " \"type\": \"tax-receipt\",\n" 129 " \"donau_urls\": [\"a\", \"b\", \"c\"],\n" 130 " \"amount\": \"KUDOS:10\"\n" 131 " }\n" 132 " ]\n" 133 " }\n" 134 " ],\n" 135 " \"token_families\": {\n" 136 " \"test-subscription\": {\n" 137 " \"name\": \"Test subscription\",\n" 138 " \"description\": \"This is a test subscription\",\n" 139 " \"description_i18n\": {\n" 140 " \"en\": \"This is a test subscription\",\n" 141 " \"es\": \"Esta es una subscripción de prueba\"\n" 142 " },\n" 143 " \"keys\": [\n" 144 " {\n" 145 " \"cipher\": \"CS\",\n" 146 " \"cs_pub\": \"AMGKHAWCF3Y32E64G6JV7TPP7KHE2C3QFMNZ8N66Q744FV3TH1D0\",\n" 147 " \"signature_validity_start\": {\"t_s\": 1736174497},\n" 148 " \"signature_validity_end\": {\"t_s\": 1736174497}\n" 149 " }\n" 150 " ],\n" 151 " \"details\": {\n" 152 " \"class\": \"subscription\",\n" 153 " \"trusted_domains\": [\"a\", \"b\", \"c\"]\n" 154 " },\n" 155 " \"critical\": true\n" 156 " },\n" 157 " \"test-discount\": {\n" 158 " \"name\": \"Test discount\",\n" 159 " \"description\": \"This is a test discount\",\n" 160 " \"description_i18n\": {\n" 161 " \"en\": \"This is a test discount\",\n" 162 " \"es\": \"Este es un descuento de prueba\"\n" 163 " },\n" 164 " \"keys\": [\n" 165 " {\n" 166 " \"cipher\": \"RSA\",\n" 167 " \"rsa_pub\": \"040000YGF5DK0PKCN99J0V814C20Q54C82S3RE3GBVC2T4QXEP7N05ABAN5DG8BC3FTN33BSG15VFX2N9X95HE7GBDAHSYHG4G00VHDCV4E0W4HVYTZGN6SGPBMTAE1XMYBH5DFWT4TXPSEQB96AG3G65X6BPQ0WXSARD5NP2YR1CQB6GB0W2BSKZK1AXZN67GHB3HHAPFV8V584QF1DGDXEWN875RN4HYNH3AW4XZ9SP5A7J5MED56P0TXX5D8C1HPWHFD89GE6Q7J0Q3QKM18WAVAZJTF6PR3Q5T2C71ST0VTP42F16ZZRWS4CHSXHM5RW0BGH383VX4100AD61X6QQ99K12Q17EQZK5MSE6AGNK24SCAH06XTXA7WFC78V0ARJKFDX1M483GE9SX20XXFKSTQ6B8104002\",\n" 168 " \"signature_validity_start\": {\"t_s\": 1736174497},\n" 169 " \"signature_validity_end\": {\"t_s\": 1736174497}\n" 170 " }\n" 171 " ],\n" 172 " \"details\": {\n" 173 " \"class\": \"discount\",\n" 174 " \"expected_domains\": [\"a\", \"b\", \"c\"]\n" 175 " },\n" 176 " \"critical\": true\n" 177 " }\n" 178 " }\n" 179 "}"; 180 181 182 int 183 main (int argc, 184 const char *const argv[]) 185 { 186 (void) argc; 187 (void) argv; 188 GNUNET_log_setup ("test-contract", 189 "WARNING", 190 NULL); 191 192 { // Contract v0 193 json_t *common; 194 json_t *v0; 195 struct TALER_MERCHANT_Contract *v0_parsed; 196 json_t *v0_serialized; 197 198 common = json_loads (contract_common, 0, NULL); 199 GNUNET_assert (NULL != common); 200 201 v0 = json_loads (contract_v0, 0, NULL); 202 GNUNET_assert (NULL != v0); 203 204 GNUNET_assert (0 == json_object_update_new (v0, common)); 205 206 v0_parsed = TALER_MERCHANT_contract_parse (v0); 207 GNUNET_assert (NULL != v0_parsed); 208 209 v0_serialized = TALER_MERCHANT_contract_serialize (v0_parsed); 210 GNUNET_assert (NULL != v0_serialized); 211 GNUNET_assert (1 == json_equal (v0, v0_serialized)); 212 213 json_decref (v0_serialized); 214 TALER_MERCHANT_contract_free (v0_parsed); 215 json_decref (v0); 216 } 217 218 { // Contract v1 219 json_t *common; 220 json_t *v1; 221 struct TALER_MERCHANT_Contract *v1_parsed; 222 json_t *v1_serialized; 223 224 common = json_loads (contract_common, 0, NULL); 225 GNUNET_assert (NULL != common); 226 227 v1 = json_loads (contract_v1, 0, NULL); 228 GNUNET_assert (NULL != v1); 229 230 GNUNET_assert (0 == json_object_update_new (v1, common)); 231 232 v1_parsed = TALER_MERCHANT_contract_parse (v1); 233 GNUNET_assert (NULL != v1_parsed); 234 235 v1_serialized = TALER_MERCHANT_contract_serialize (v1_parsed); 236 GNUNET_assert (NULL != v1_serialized); 237 238 GNUNET_assert (1 == json_equal (v1, v1_serialized)); 239 240 for (unsigned int i = 0; 241 i < v1_parsed->pc->details.v1.token_authorities_len; 242 i++) 243 { 244 struct TALER_MERCHANT_ContractTokenFamily *in = 245 &v1_parsed->pc->details.v1.token_authorities[i]; 246 for (unsigned int j = 0; j < in->keys_len; j++) 247 { 248 struct TALER_MERCHANT_ContractTokenFamilyKey *inkey = &in->keys[j]; 249 struct TALER_MERCHANT_ContractTokenFamily out; 250 struct TALER_MERCHANT_ContractTokenFamilyKey outkey; 251 GNUNET_assert (GNUNET_OK == TALER_MERCHANT_find_token_family_key ( 252 in->slug, 253 inkey->valid_after, 254 v1_parsed->pc->details.v1.token_authorities, 255 v1_parsed->pc->details.v1.token_authorities_len, 256 &out, 257 &outkey)); 258 GNUNET_assert (0 == GNUNET_memcmp (in, &out)); 259 GNUNET_assert (0 == GNUNET_memcmp (inkey, &outkey)); 260 } 261 } 262 263 json_decref (v1_serialized); 264 TALER_MERCHANT_contract_free (v1_parsed); 265 json_decref (v1); 266 } 267 268 { // Template pay duration validation 269 json_t *finite; 270 json_t *zero; 271 json_t *omitted; 272 json_t *forever; 273 struct TALER_MERCHANT_TemplateContract parsed = { 0 }; 274 275 finite = json_pack ("{s:{s:I}}", 276 "pay_duration", 277 "d_us", 278 (json_int_t) GNUNET_TIME_UNIT_MINUTES.rel_value_us); 279 zero = json_pack ("{s:{s:I}}", 280 "pay_duration", 281 "d_us", 282 (json_int_t) 0); 283 omitted = json_object (); 284 forever = json_pack ("{s:{s:s}}", 285 "pay_duration", 286 "d_us", 287 "forever"); 288 GNUNET_assert (TALER_MERCHANT_template_contract_valid (finite)); 289 GNUNET_assert (TALER_MERCHANT_template_contract_valid (zero)); 290 GNUNET_assert (TALER_MERCHANT_template_contract_valid (omitted)); 291 GNUNET_assert (! TALER_MERCHANT_template_contract_valid (forever)); 292 /* Legacy templates must remain parseable so they can fall back to the 293 instance default when instantiated. */ 294 GNUNET_assert ( 295 GNUNET_OK == 296 TALER_MERCHANT_template_contract_parse (forever, 297 &parsed, 298 NULL)); 299 GNUNET_assert (GNUNET_TIME_relative_is_forever (parsed.pay_duration)); 300 TALER_MERCHANT_template_contract_free (&parsed); 301 json_decref (finite); 302 json_decref (zero); 303 json_decref (omitted); 304 json_decref (forever); 305 } 306 307 { // amount_external currency must match all v1 choices 308 json_t *common; 309 json_t *v1; 310 json_t *bad_external; 311 312 common = json_loads (contract_common, 0, NULL); 313 GNUNET_assert (NULL != common); 314 v1 = json_loads (contract_v1, 0, NULL); 315 GNUNET_assert (NULL != v1); 316 GNUNET_assert (0 == json_object_update_new (v1, common)); 317 bad_external = json_loads ( 318 "[{\"method\":\"cash\",\"id\":\"cash1\",\"amount\":\"EUR:3\"}]", 319 0, 320 NULL); 321 GNUNET_assert (NULL != bad_external); 322 GNUNET_assert (0 == json_object_set_new (v1, 323 "amount_external", 324 bad_external)); 325 GNUNET_assert (NULL == TALER_MERCHANT_contract_parse (v1)); 326 json_decref (v1); 327 } 328 329 { // amount_external validation 330 struct TALER_Amount amount; 331 static const char *invalid[] = { 332 /* method "taler" is reserved */ 333 "[{\"method\":\"taler\",\"id\":\"t1\",\"amount\":\"KUDOS:1\"}]", 334 /* entries disagreeing on a currency among themselves */ 335 "[{\"method\":\"cash\",\"id\":\"cash1\",\"amount\":\"KUDOS:1\"}," 336 " {\"method\":\"card\",\"id\":\"card1\",\"amount\":\"EUR:1\"}]", 337 /* missing id */ 338 "[{\"method\":\"cash\",\"amount\":\"KUDOS:1\"}]", 339 /* missing amount */ 340 "[{\"method\":\"cash\",\"id\":\"cash1\"}]", 341 /* empty method name */ 342 "[{\"method\":\"\",\"id\":\"cash1\",\"amount\":\"KUDOS:1\"}]", 343 /* method name with blank */ 344 "[{\"method\":\"ca sh\",\"id\":\"cash1\",\"amount\":\"KUDOS:1\"}]", 345 /* duplicate id */ 346 "[{\"method\":\"cash\",\"id\":\"cash1\",\"amount\":\"KUDOS:1\"}," 347 " {\"method\":\"card\",\"id\":\"cash1\",\"amount\":\"KUDOS:1\"}]", 348 /* nested method-specific field */ 349 "[{\"method\":\"cash\",\"id\":\"cash1\",\"amount\":\"KUDOS:1\"," 350 " \"details\":{\"register\":1}}]", 351 NULL 352 }; 353 354 GNUNET_assert (GNUNET_OK == 355 TALER_string_to_amount ("KUDOS:10", 356 &amount)); 357 { 358 json_t *valid; 359 360 valid = json_loads ( 361 "[{\"method\":\"cash\",\"id\":\"cash1\",\"amount\":\"KUDOS:30\"," 362 " \"cashier_number\":\"7\"}]", 363 0, 364 NULL); 365 GNUNET_assert (NULL != valid); 366 GNUNET_assert (TALER_MERCHANT_amount_external_valid (valid)); 367 /* valid entries in the currency of the order, and not in another */ 368 GNUNET_assert (TALER_MERCHANT_amount_external_currency_valid (valid, 369 &amount)); 370 { 371 struct TALER_Amount other; 372 373 GNUNET_assert (GNUNET_OK == 374 TALER_string_to_amount ("EUR:10", 375 &other)); 376 GNUNET_assert (! TALER_MERCHANT_amount_external_currency_valid (valid, 377 &other)); 378 } 379 GNUNET_assert (TALER_MERCHANT_amount_external_total_valid (valid, 380 &amount)); 381 json_decref (valid); 382 } 383 { // the order total must stay representable 384 json_t *huge; 385 struct TALER_Amount big; 386 387 /* Two entries just below the largest representable amount: 388 each is fine on its own, their sum is not. */ 389 huge = json_loads ( 390 "[{\"method\":\"cash\",\"id\":\"cash1\"," 391 " \"amount\":\"KUDOS:4503599627370495\"}," 392 " {\"method\":\"card\",\"id\":\"card1\"," 393 " \"amount\":\"KUDOS:4503599627370495\"}]", 394 0, 395 NULL); 396 GNUNET_assert (NULL != huge); 397 /* the entries themselves are perfectly valid */ 398 GNUNET_assert (TALER_MERCHANT_amount_external_valid (huge)); 399 GNUNET_assert (TALER_MERCHANT_amount_external_currency_valid (huge, 400 &amount)); 401 /* ... but adding them up overflows */ 402 GNUNET_assert (! TALER_MERCHANT_amount_external_total_valid (huge, 403 &amount)); 404 json_decref (huge); 405 406 /* A single entry that overflows only once the Taler amount of 407 the order is added to it. */ 408 huge = json_loads ( 409 "[{\"method\":\"cash\",\"id\":\"cash1\"," 410 " \"amount\":\"KUDOS:4503599627370495\"}]", 411 0, 412 NULL); 413 GNUNET_assert (NULL != huge); 414 GNUNET_assert (GNUNET_OK == 415 TALER_string_to_amount ("KUDOS:10", 416 &big)); 417 GNUNET_assert (! TALER_MERCHANT_amount_external_total_valid (huge, 418 &big)); 419 json_decref (huge); 420 } 421 for (unsigned int i = 0; NULL != invalid[i]; i++) 422 { 423 json_t *j; 424 425 j = json_loads (invalid[i], 0, NULL); 426 GNUNET_assert (NULL != j); 427 GNUNET_assert (! TALER_MERCHANT_amount_external_valid (j)); 428 json_decref (j); 429 } 430 } 431 432 return 0; 433 }