test_purse_decision.c (12590B)
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_decision.c 18 * @brief tests for the exchangedb functions whose primary table is 19 * `purse_decision` 20 * @author Christian Grothoff 21 * 22 * Covers #TALER_EXCHANGEDB_do_expire_purse(), 23 * #TALER_EXCHANGEDB_iterate_purse_decisions_above_serial_id() and 24 * #TALER_EXCHANGEDB_iterate_all_purse_decisions_above_serial_id(). 25 * 26 * A purse is decided either because it was merged or because it expired. 27 * do_expire_purse() handles the second case: it decides the oldest expired 28 * purse in the window it is given, marks the decision as a refund and 29 * gives the coins their money back -- which is what the checks look at. 30 */ 31 #include "test_common.h" 32 #include "exchange-database/do_expire_purse.h" 33 #include "exchange-database/do_purse_deposit.h" 34 #include "exchange-database/iterate_all_purse_decisions_above_serial_id.h" 35 #include "exchange-database/iterate_purse_decisions_above_serial_id.h" 36 37 38 /** 39 * Denomination the checks deposit into purses. 40 */ 41 static struct TDB_Denom denom; 42 43 44 /** 45 * Build a timestamp from a number of seconds since the epoch. 46 * 47 * @param secs seconds since the epoch 48 * @return the timestamp 49 */ 50 static struct GNUNET_TIME_Timestamp 51 ts (uint64_t secs) 52 { 53 struct GNUNET_TIME_Absolute abs = { 54 .abs_value_us = secs * 1000LLU * 1000LLU 55 }; 56 57 return GNUNET_TIME_absolute_to_timestamp (abs); 58 } 59 60 61 /** 62 * Closure for #decision_cb() and #all_decision_cb(). 63 */ 64 struct DecisionContext 65 { 66 /** 67 * How many rows did the callback see? 68 */ 69 unsigned int total; 70 71 /** 72 * Stop after this many rows; 0 for no limit. 73 */ 74 unsigned int stop_after; 75 76 /** 77 * Purse we are looking for, NULL to match nothing. 78 */ 79 const struct TALER_PurseContractPublicKeyP *purse_pub; 80 81 /** 82 * How many times did we see it? 83 */ 84 unsigned int matched; 85 86 /** 87 * Was it reported as refunded? 88 */ 89 bool refunded; 90 91 /** 92 * Was a reserve reported for it? 93 */ 94 bool have_reserve; 95 }; 96 97 98 /** 99 * Callback for 100 * #TALER_EXCHANGEDB_iterate_purse_decisions_above_serial_id(). 101 * 102 * @param cls a `struct DecisionContext *` 103 * @param rowid row of the decision 104 * @param purse_pub the purse 105 * @param reserve_pub reserve it went to, NULL if refunded 106 * @param purse_value target value of the purse 107 * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop 108 */ 109 static enum GNUNET_GenericReturnValue 110 decision_cb (void *cls, 111 uint64_t rowid, 112 const struct TALER_PurseContractPublicKeyP *purse_pub, 113 const struct TALER_ReservePublicKeyP *reserve_pub, 114 const struct TALER_Amount *purse_value) 115 { 116 struct DecisionContext *ctx = cls; 117 118 (void) rowid; 119 (void) purse_value; 120 ctx->total++; 121 if ( (NULL != ctx->purse_pub) && 122 (0 == GNUNET_memcmp (purse_pub, 123 ctx->purse_pub)) ) 124 { 125 ctx->matched++; 126 ctx->have_reserve = (NULL != reserve_pub); 127 } 128 if ( (0 != ctx->stop_after) && 129 (ctx->total >= ctx->stop_after) ) 130 return GNUNET_SYSERR; 131 return GNUNET_OK; 132 } 133 134 135 /** 136 * Callback for 137 * #TALER_EXCHANGEDB_iterate_all_purse_decisions_above_serial_id(). 138 * 139 * @param cls a `struct DecisionContext *` 140 * @param rowid row of the decision 141 * @param purse_pub the purse 142 * @param refunded whether the decision was to refund 143 * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop 144 */ 145 static enum GNUNET_GenericReturnValue 146 all_decision_cb (void *cls, 147 uint64_t rowid, 148 const struct TALER_PurseContractPublicKeyP *purse_pub, 149 bool refunded) 150 { 151 struct DecisionContext *ctx = cls; 152 153 (void) rowid; 154 ctx->total++; 155 if ( (NULL != ctx->purse_pub) && 156 (0 == GNUNET_memcmp (purse_pub, 157 ctx->purse_pub)) ) 158 { 159 ctx->matched++; 160 ctx->refunded = refunded; 161 } 162 if ( (0 != ctx->stop_after) && 163 (ctx->total >= ctx->stop_after) ) 164 return GNUNET_SYSERR; 165 return GNUNET_OK; 166 } 167 168 169 /** 170 * With no purse to expire, nothing is decided and nothing is reported. 171 * 172 * @param pg the database context 173 * @return 0 on success 174 */ 175 static int 176 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg) 177 { 178 struct DecisionContext ctx = { 0 }; 179 struct GNUNET_TIME_Absolute expiration; 180 181 TDB_denom (pg, 182 10, 183 "5", 184 "0.1", 185 &denom); 186 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 187 TALER_EXCHANGEDB_do_expire_purse (pg, 188 GNUNET_TIME_UNIT_ZERO_ABS, 189 GNUNET_TIME_absolute_get (), 190 &expiration)); 191 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 192 TALER_EXCHANGEDB_iterate_purse_decisions_above_serial_id ( 193 pg, 194 0, 195 false, 196 &decision_cb, 197 &ctx)); 198 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 199 TALER_EXCHANGEDB_iterate_all_purse_decisions_above_serial_id ( 200 pg, 201 0, 202 &all_decision_cb, 203 &ctx)); 204 FAILIF (0 != ctx.total); 205 return 0; 206 } 207 208 209 /** 210 * A purse that has not expired yet is left alone. 211 * 212 * @param pg the database context 213 * @return 0 on success 214 */ 215 static int 216 check_not_expired (struct TALER_EXCHANGEDB_PostgresContext *pg) 217 { 218 struct TDB_Purse purse; 219 struct GNUNET_TIME_Absolute expiration; 220 221 /* expires in 2023 */ 222 TDB_purse (pg, 223 10, 224 "5", 225 ts (1700000000), 226 &purse); 227 /* ...but we only look at purses that expired before 2020 */ 228 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 229 TALER_EXCHANGEDB_do_expire_purse (pg, 230 GNUNET_TIME_UNIT_ZERO_ABS, 231 ts (1600000000).abs_time, 232 &expiration)); 233 FAILIF (0 != TDB_count (pg, 234 "FROM purse_decision")); 235 return 0; 236 } 237 238 239 /** 240 * Expiring a purse decides it as a refund and gives the coins their 241 * money back. 242 * 243 * @param pg the database context 244 * @return 0 on success 245 */ 246 static int 247 check_expire (struct TALER_EXCHANGEDB_PostgresContext *pg) 248 { 249 struct TDB_Purse purse; 250 struct TALER_CoinPublicInfo coin; 251 struct GNUNET_TIME_Absolute expiration; 252 struct DecisionContext ctx; 253 struct TALER_CoinSpendSignatureP coin_sig; 254 struct TALER_Amount amount = TDB_amount ("1"); 255 bool balance_ok; 256 bool too_late; 257 bool conflict; 258 char *hex; 259 260 /* a purse that expired in 2020, with a coin paid into it */ 261 TDB_purse (pg, 262 11, 263 "5", 264 ts (1600000000), 265 &purse); 266 TDB_coin (pg, 267 &denom, 268 20, 269 &coin, 270 NULL); 271 TDB_FILL (coin_sig, 272 20); 273 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 274 TALER_EXCHANGEDB_do_purse_deposit (pg, 275 &purse.purse_pub, 276 &coin.coin_pub, 277 &amount, 278 &coin_sig, 279 &amount, 280 &balance_ok, 281 &too_late, 282 &conflict), 283 TDB_coin_free (&coin)); 284 FAILIF_C (! balance_ok, 285 TDB_coin_free (&coin)); 286 hex = TDB_hex (&coin.coin_pub, 287 sizeof (coin.coin_pub)); 288 FAILIF_C (1 != TDB_count (pg, 289 "FROM known_coins" 290 " WHERE coin_pub=decode('%s','hex')" 291 " AND remaining=ROW(4,0)::taler_amount", 292 hex), 293 GNUNET_free (hex); TDB_coin_free (&coin)); 294 295 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 296 TALER_EXCHANGEDB_do_expire_purse (pg, 297 GNUNET_TIME_UNIT_ZERO_ABS, 298 GNUNET_TIME_absolute_get (), 299 &expiration), 300 GNUNET_free (hex); TDB_coin_free (&coin)); 301 FAILIF_C (expiration.abs_value_us != 302 ts (1600000000).abs_time.abs_value_us, 303 GNUNET_free (hex); TDB_coin_free (&coin)); 304 FAILIF_C (1 != TDB_count (pg, 305 "FROM purse_decision"), 306 GNUNET_free (hex); TDB_coin_free (&coin)); 307 /* the coin got its money back */ 308 FAILIF_C (1 != TDB_count (pg, 309 "FROM known_coins" 310 " WHERE coin_pub=decode('%s','hex')" 311 " AND remaining=ROW(5,0)::taler_amount", 312 hex), 313 GNUNET_free (hex); TDB_coin_free (&coin)); 314 GNUNET_free (hex); 315 TDB_coin_free (&coin); 316 317 /* the decision is a refund, so the by-refund iterator sees it... */ 318 memset (&ctx, 319 0, 320 sizeof (ctx)); 321 ctx.purse_pub = &purse.purse_pub; 322 FAILIF (0 >= 323 TALER_EXCHANGEDB_iterate_purse_decisions_above_serial_id ( 324 pg, 325 0, 326 true, 327 &decision_cb, 328 &ctx)); 329 FAILIF (1 != ctx.matched); 330 /* ...a refunded purse has no reserve to credit */ 331 FAILIF (ctx.have_reserve); 332 333 /* ...and the non-refunded view does not */ 334 memset (&ctx, 335 0, 336 sizeof (ctx)); 337 ctx.purse_pub = &purse.purse_pub; 338 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 339 TALER_EXCHANGEDB_iterate_purse_decisions_above_serial_id ( 340 pg, 341 0, 342 false, 343 &decision_cb, 344 &ctx)); 345 FAILIF (0 != ctx.matched); 346 347 /* the all-decisions view reports it with its refund flag */ 348 memset (&ctx, 349 0, 350 sizeof (ctx)); 351 ctx.purse_pub = &purse.purse_pub; 352 FAILIF (0 >= 353 TALER_EXCHANGEDB_iterate_all_purse_decisions_above_serial_id ( 354 pg, 355 0, 356 &all_decision_cb, 357 &ctx)); 358 FAILIF (1 != ctx.matched); 359 FAILIF (! ctx.refunded); 360 361 /* a second sweep finds nothing left to expire */ 362 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 363 TALER_EXCHANGEDB_do_expire_purse (pg, 364 GNUNET_TIME_UNIT_ZERO_ABS, 365 ts (1650000000).abs_time, 366 &expiration)); 367 FAILIF (1 != TDB_count (pg, 368 "FROM purse_decision")); 369 return 0; 370 } 371 372 373 /** 374 * The iterators' serial bounds and abort returns behave as documented. 375 * 376 * @param pg the database context 377 * @return 0 on success 378 */ 379 static int 380 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg) 381 { 382 struct DecisionContext ctx; 383 384 memset (&ctx, 385 0, 386 sizeof (ctx)); 387 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 388 TALER_EXCHANGEDB_iterate_all_purse_decisions_above_serial_id ( 389 pg, 390 1000, 391 &all_decision_cb, 392 &ctx)); 393 FAILIF (0 != ctx.total); 394 memset (&ctx, 395 0, 396 sizeof (ctx)); 397 ctx.stop_after = 1; 398 FAILIF (1 != 399 TALER_EXCHANGEDB_iterate_all_purse_decisions_above_serial_id ( 400 pg, 401 0, 402 &all_decision_cb, 403 &ctx)); 404 FAILIF (1 != ctx.total); 405 return 0; 406 } 407 408 409 /** 410 * The checks to run, in order. 411 */ 412 static const struct TDB_Test tests[] = { 413 { "purse-decision-empty", 414 &check_empty }, 415 { "purse-decision-not-expired", 416 &check_not_expired }, 417 { "purse-decision-expire", 418 &check_expire }, 419 { "purse-decision-iterate", 420 &check_iterate }, 421 { NULL, NULL } 422 }; 423 424 425 int 426 main (int argc, 427 char *const *argv) 428 { 429 int ret; 430 431 ret = TDB_main (argc, 432 argv, 433 "test-purse-decision", 434 "Tests for the exchangedb `purse_decision' table", 435 tests); 436 TDB_denom_free (&denom); 437 return ret; 438 } 439 440 441 /* end of test_purse_decision.c */