test_purse_requests.c (15590B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2026 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 * @file exchangedb/test_purse_requests.c 18 * @brief tests for the exchangedb functions whose primary table is 19 * `purse_requests` 20 * @author Christian Grothoff 21 * 22 * Covers #TALER_EXCHANGEDB_insert_purse_request(), 23 * #TALER_EXCHANGEDB_get_purse_request(), 24 * #TALER_EXCHANGEDB_get_purse_by_merge_pub(), 25 * #TALER_EXCHANGEDB_get_purse() and 26 * #TALER_EXCHANGEDB_iterate_purse_requests_above_serial_id(). 27 * 28 * `purse_requests` has no foreign keys -- a purse is created before 29 * anything is paid into it. The interesting case of the insert is the 30 * conflict: re-creating a purse with the same key but different meta data 31 * is reported through @e in_conflict rather than as an error. 32 */ 33 #include "test_common.h" 34 #include "exchange-database/get_purse.h" 35 #include "exchange-database/get_purse_by_merge_pub.h" 36 #include "exchange-database/get_purse_request.h" 37 #include "exchange-database/insert_purse_request.h" 38 #include "exchange-database/iterate_purse_requests_above_serial_id.h" 39 40 41 /** 42 * Build a timestamp from a number of seconds since the epoch. 43 * 44 * @param secs seconds since the epoch 45 * @return the timestamp 46 */ 47 static struct GNUNET_TIME_Timestamp 48 ts (uint64_t secs) 49 { 50 struct GNUNET_TIME_Absolute abs = { 51 .abs_value_us = secs * 1000LLU * 1000LLU 52 }; 53 54 return GNUNET_TIME_absolute_to_timestamp (abs); 55 } 56 57 58 /** 59 * Closure for #purse_cb(). 60 */ 61 struct PurseContext 62 { 63 /** 64 * How many rows did the callback see? 65 */ 66 unsigned int total; 67 68 /** 69 * Stop after this many rows; 0 for no limit. 70 */ 71 unsigned int stop_after; 72 73 /** 74 * Purse we are looking for, NULL to match nothing. 75 */ 76 const struct TALER_PurseContractPublicKeyP *purse_pub; 77 78 /** 79 * How many times did we see it? 80 */ 81 unsigned int matched; 82 83 /** 84 * Target amount reported for it. 85 */ 86 struct TALER_Amount amount; 87 88 /** 89 * Age limit reported for it. 90 */ 91 uint32_t age_limit; 92 }; 93 94 95 /** 96 * Callback for #TALER_EXCHANGEDB_iterate_purse_requests_above_serial_id(). 97 * 98 * @param cls a `struct PurseContext *` 99 * @param rowid row of the purse 100 * @param purse_pub the purse 101 * @param merge_pub its merge capability key 102 * @param purse_creation when it was created 103 * @param purse_expiration when it expires 104 * @param h_contract_terms contract it is for 105 * @param age_limit age limit on deposits into it 106 * @param target_amount how much it is meant to hold 107 * @param purse_sig signature over its meta data 108 * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop 109 */ 110 static enum GNUNET_GenericReturnValue 111 purse_cb (void *cls, 112 uint64_t rowid, 113 const struct TALER_PurseContractPublicKeyP *purse_pub, 114 const struct TALER_PurseMergePublicKeyP *merge_pub, 115 struct GNUNET_TIME_Timestamp purse_creation, 116 struct GNUNET_TIME_Timestamp purse_expiration, 117 const struct TALER_PrivateContractHashP *h_contract_terms, 118 uint32_t age_limit, 119 const struct TALER_Amount *target_amount, 120 const struct TALER_PurseContractSignatureP *purse_sig) 121 { 122 struct PurseContext *ctx = cls; 123 124 (void) rowid; 125 (void) merge_pub; 126 (void) purse_creation; 127 (void) purse_expiration; 128 (void) h_contract_terms; 129 (void) purse_sig; 130 ctx->total++; 131 if ( (NULL != ctx->purse_pub) && 132 (0 == GNUNET_memcmp (purse_pub, 133 ctx->purse_pub)) ) 134 { 135 ctx->matched++; 136 ctx->amount = *target_amount; 137 ctx->age_limit = age_limit; 138 } 139 if ( (0 != ctx->stop_after) && 140 (ctx->total >= ctx->stop_after) ) 141 return GNUNET_SYSERR; 142 return GNUNET_OK; 143 } 144 145 146 /** 147 * Nothing is known while the table is empty. 148 * 149 * @param pg the database context 150 * @return 0 on success 151 */ 152 static int 153 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg) 154 { 155 struct TALER_PurseContractPublicKeyP purse_pub; 156 struct TALER_PurseMergePublicKeyP merge_pub; 157 struct GNUNET_TIME_Timestamp purse_expiration; 158 struct GNUNET_TIME_Timestamp purse_creation; 159 struct GNUNET_TIME_Timestamp merge_timestamp; 160 struct TALER_PrivateContractHashP h_contract_terms; 161 struct TALER_Amount target_amount; 162 struct TALER_Amount balance; 163 struct TALER_PurseContractSignatureP purse_sig; 164 struct PurseContext ctx = { 0 }; 165 uint32_t age_limit; 166 bool purse_deleted; 167 bool purse_refunded; 168 169 TDB_FILL (purse_pub, 170 1); 171 TDB_FILL (merge_pub, 172 1); 173 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 174 TALER_EXCHANGEDB_get_purse_request (pg, 175 &purse_pub, 176 &merge_pub, 177 &purse_expiration, 178 &h_contract_terms, 179 &age_limit, 180 &target_amount, 181 &balance, 182 &purse_sig)); 183 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 184 TALER_EXCHANGEDB_get_purse_by_merge_pub (pg, 185 &merge_pub, 186 &purse_pub, 187 &purse_expiration, 188 &h_contract_terms, 189 &age_limit, 190 &target_amount, 191 &balance, 192 &purse_sig)); 193 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 194 TALER_EXCHANGEDB_get_purse (pg, 195 &purse_pub, 196 &purse_creation, 197 &purse_expiration, 198 &target_amount, 199 &balance, 200 &h_contract_terms, 201 &merge_timestamp, 202 &purse_deleted, 203 &purse_refunded)); 204 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 205 TALER_EXCHANGEDB_iterate_purse_requests_above_serial_id ( 206 pg, 207 0, 208 &purse_cb, 209 &ctx)); 210 FAILIF (0 != ctx.total); 211 return 0; 212 } 213 214 215 /** 216 * A created purse is found by its own key, by its merge key and by the 217 * combined status lookup. 218 * 219 * @param pg the database context 220 * @return 0 on success 221 */ 222 static int 223 check_create_and_lookup (struct TALER_EXCHANGEDB_PostgresContext *pg) 224 { 225 struct TDB_Purse purse; 226 struct TALER_PurseContractPublicKeyP got_pub; 227 struct TALER_PurseMergePublicKeyP got_merge; 228 struct GNUNET_TIME_Timestamp purse_expiration; 229 struct GNUNET_TIME_Timestamp purse_creation; 230 struct GNUNET_TIME_Timestamp merge_timestamp; 231 struct TALER_PrivateContractHashP h_contract_terms; 232 struct TALER_Amount target_amount; 233 struct TALER_Amount balance; 234 struct TALER_Amount zero = TDB_amount ("0"); 235 struct TALER_PurseContractSignatureP purse_sig; 236 uint32_t age_limit; 237 bool purse_deleted = true; 238 bool purse_refunded = true; 239 240 TDB_purse (pg, 241 10, 242 "5", 243 ts (1700000000), 244 &purse); 245 FAILIF (1 != TDB_count (pg, 246 "FROM purse_requests")); 247 248 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 249 TALER_EXCHANGEDB_get_purse_request (pg, 250 &purse.purse_pub, 251 &got_merge, 252 &purse_expiration, 253 &h_contract_terms, 254 &age_limit, 255 &target_amount, 256 &balance, 257 &purse_sig)); 258 FAILIF (0 != GNUNET_memcmp (&got_merge, 259 &purse.merge_pub)); 260 FAILIF (0 != GNUNET_memcmp (&h_contract_terms, 261 &purse.h_contract_terms)); 262 FAILIF (0 != GNUNET_memcmp (&purse_sig, 263 &purse.purse_sig)); 264 FAILIF (GNUNET_TIME_timestamp_cmp (purse_expiration, 265 !=, 266 purse.purse_expiration)); 267 FAILIF (0 != TALER_amount_cmp (&target_amount, 268 &purse.amount)); 269 /* nothing has been paid in yet */ 270 FAILIF (0 != TALER_amount_cmp (&balance, 271 &zero)); 272 FAILIF (0 != age_limit); 273 274 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 275 TALER_EXCHANGEDB_get_purse_by_merge_pub (pg, 276 &purse.merge_pub, 277 &got_pub, 278 &purse_expiration, 279 &h_contract_terms, 280 &age_limit, 281 &target_amount, 282 &balance, 283 &purse_sig)); 284 FAILIF (0 != GNUNET_memcmp (&got_pub, 285 &purse.purse_pub)); 286 287 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 288 TALER_EXCHANGEDB_get_purse (pg, 289 &purse.purse_pub, 290 &purse_creation, 291 &purse_expiration, 292 &target_amount, 293 &balance, 294 &h_contract_terms, 295 &merge_timestamp, 296 &purse_deleted, 297 &purse_refunded)); 298 FAILIF (purse_deleted); 299 FAILIF (purse_refunded); 300 FAILIF (0 != TALER_amount_cmp (&balance, 301 &zero)); 302 /* the purse has not been merged */ 303 FAILIF (! GNUNET_TIME_absolute_is_never (merge_timestamp.abs_time)); 304 305 /* a merge key nobody used finds nothing */ 306 { 307 struct TALER_PurseMergePublicKeyP other; 308 309 TDB_FILL (other, 310 99); 311 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 312 TALER_EXCHANGEDB_get_purse_by_merge_pub (pg, 313 &other, 314 &got_pub, 315 &purse_expiration, 316 &h_contract_terms, 317 &age_limit, 318 &target_amount, 319 &balance, 320 &purse_sig)); 321 } 322 return 0; 323 } 324 325 326 /** 327 * Re-creating a purse with the same key is idempotent if the meta data 328 * matches and a conflict if it does not. 329 * 330 * @param pg the database context 331 * @return 0 on success 332 */ 333 static int 334 check_conflict (struct TALER_EXCHANGEDB_PostgresContext *pg) 335 { 336 struct TDB_Purse purse; 337 struct TALER_Amount purse_fee = TDB_amount ("0"); 338 struct TALER_Amount other_amount = TDB_amount ("9"); 339 bool in_conflict = false; 340 341 /* TDB_purse() with the same seed replays the identical request */ 342 TDB_purse (pg, 343 10, 344 "5", 345 ts (1700000000), 346 &purse); 347 FAILIF (1 != TDB_count (pg, 348 "FROM purse_requests")); 349 /* An identical replay is reported as "nothing to do". Note that the 350 function leaves @e in_conflict untouched on that path, so only the 351 status says so. */ 352 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 353 TALER_EXCHANGEDB_insert_purse_request ( 354 pg, 355 &purse.purse_pub, 356 &purse.merge_pub, 357 purse.purse_expiration, 358 &purse.h_contract_terms, 359 0, 360 TALER_WAMF_MODE_CREATE_FROM_PURSE_QUOTA, 361 &purse_fee, 362 &purse.amount, 363 &purse.purse_sig, 364 &in_conflict)); 365 FAILIF (in_conflict); 366 367 /* the same purse key with a different target amount is a conflict */ 368 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 369 TALER_EXCHANGEDB_insert_purse_request ( 370 pg, 371 &purse.purse_pub, 372 &purse.merge_pub, 373 purse.purse_expiration, 374 &purse.h_contract_terms, 375 0, 376 TALER_WAMF_MODE_CREATE_FROM_PURSE_QUOTA, 377 &purse_fee, 378 &other_amount, 379 &purse.purse_sig, 380 &in_conflict)); 381 FAILIF (! in_conflict); 382 FAILIF (1 != TDB_count (pg, 383 "FROM purse_requests")); 384 return 0; 385 } 386 387 388 /** 389 * The iterator walks the table by serial and honours an aborting 390 * callback. 391 * 392 * @param pg the database context 393 * @return 0 on success 394 */ 395 static int 396 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg) 397 { 398 struct TDB_Purse p2; 399 struct PurseContext ctx; 400 401 TDB_purse (pg, 402 11, 403 "3", 404 ts (1700003600), 405 &p2); 406 FAILIF (2 != TDB_count (pg, 407 "FROM purse_requests")); 408 memset (&ctx, 409 0, 410 sizeof (ctx)); 411 ctx.purse_pub = &p2.purse_pub; 412 FAILIF (2 != 413 TALER_EXCHANGEDB_iterate_purse_requests_above_serial_id ( 414 pg, 415 0, 416 &purse_cb, 417 &ctx)); 418 FAILIF (1 != ctx.matched); 419 FAILIF (0 != TALER_amount_cmp (&ctx.amount, 420 &p2.amount)); 421 FAILIF (0 != ctx.age_limit); 422 423 memset (&ctx, 424 0, 425 sizeof (ctx)); 426 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 427 TALER_EXCHANGEDB_iterate_purse_requests_above_serial_id ( 428 pg, 429 1000, 430 &purse_cb, 431 &ctx)); 432 FAILIF (0 != ctx.total); 433 434 memset (&ctx, 435 0, 436 sizeof (ctx)); 437 ctx.stop_after = 1; 438 FAILIF (2 != 439 TALER_EXCHANGEDB_iterate_purse_requests_above_serial_id ( 440 pg, 441 0, 442 &purse_cb, 443 &ctx)); 444 FAILIF (1 != ctx.total); 445 return 0; 446 } 447 448 449 /** 450 * The checks to run, in order. 451 */ 452 static const struct TDB_Test tests[] = { 453 { "purse-requests-empty", 454 &check_empty }, 455 { "purse-requests-create-and-lookup", 456 &check_create_and_lookup }, 457 { "purse-requests-conflict", 458 &check_conflict }, 459 { "purse-requests-iterate", 460 &check_iterate }, 461 { NULL, NULL } 462 }; 463 464 465 int 466 main (int argc, 467 char *const *argv) 468 { 469 return TDB_main (argc, 470 argv, 471 "test-purse-requests", 472 "Tests for the exchangedb `purse_requests' table", 473 tests); 474 } 475 476 477 /* end of test_purse_requests.c */