test_close_requests.c (14973B)
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_close_requests.c 18 * @brief tests for the exchangedb functions whose primary table is 19 * `close_requests` 20 * @author Christian Grothoff 21 * 22 * Covers #TALER_EXCHANGEDB_insert_close_request(), 23 * #TALER_EXCHANGEDB_get_reserve_close_request_info() and 24 * #TALER_EXCHANGEDB_iterate_unfinished_close_requests(). 25 * 26 * The table references `reserves`, which TDB_reserve() creates. The 27 * iterator is destructive -- it marks everything it returns as done -- so 28 * the checks that read the table run before it. 29 */ 30 #include "test_common.h" 31 #include "exchange-database/insert_close_request.h" 32 #include "exchange-database/get_reserve_close_request_info.h" 33 #include "exchange-database/iterate_unfinished_close_requests.h" 34 35 36 /** 37 * Account the checks ask to be paid out to. 38 */ 39 #define PAYTO "payto://x-taler-bank/localhost/cr?receiver-name=CR" 40 41 42 /** 43 * Build a timestamp from a number of seconds since the epoch. 44 * 45 * @param secs seconds since the epoch 46 * @return the timestamp 47 */ 48 static struct GNUNET_TIME_Timestamp 49 ts (uint64_t secs) 50 { 51 struct GNUNET_TIME_Absolute abs = { 52 .abs_value_us = secs * 1000LLU * 1000LLU 53 }; 54 55 return GNUNET_TIME_absolute_to_timestamp (abs); 56 } 57 58 59 /** 60 * Closure for #expired_cb(). 61 */ 62 struct ExpiredContext 63 { 64 /** 65 * How many requests did the callback see? 66 */ 67 unsigned int total; 68 69 /** 70 * Reserve of the last request seen. 71 */ 72 struct TALER_ReservePublicKeyP reserve_pub; 73 74 /** 75 * Balance of the last request seen. 76 */ 77 struct TALER_Amount left; 78 79 /** 80 * Row of the last request seen. 81 */ 82 uint64_t close_request_row; 83 84 /** 85 * Account of the last request seen, owned by this struct. 86 */ 87 char *account; 88 }; 89 90 91 /** 92 * Callback for #TALER_EXCHANGEDB_iterate_unfinished_close_requests(). 93 * 94 * @param cls a `struct ExpiredContext *` 95 * @param reserve_pub the reserve to close 96 * @param left balance to pay out 97 * @param account_details where to pay it 98 * @param expiration_date when the request was made 99 * @param close_request_row row of the request 100 * @return #GNUNET_OK 101 */ 102 static enum GNUNET_GenericReturnValue 103 expired_cb (void *cls, 104 const struct TALER_ReservePublicKeyP *reserve_pub, 105 const struct TALER_Amount *left, 106 const struct TALER_FullPayto account_details, 107 struct GNUNET_TIME_Timestamp expiration_date, 108 uint64_t close_request_row) 109 { 110 struct ExpiredContext *ctx = cls; 111 112 (void) expiration_date; 113 ctx->total++; 114 ctx->reserve_pub = *reserve_pub; 115 ctx->left = *left; 116 ctx->close_request_row = close_request_row; 117 GNUNET_free (ctx->account); 118 ctx->account = (NULL == account_details.full_payto) 119 ? NULL 120 : GNUNET_strdup (account_details.full_payto); 121 return GNUNET_OK; 122 } 123 124 125 /** 126 * Look up the row of the close request of a reserve. 127 * 128 * @param pg the database context 129 * @param reserve_pub reserve to look up 130 * @return the row 131 */ 132 static uint64_t 133 request_row (struct TALER_EXCHANGEDB_PostgresContext *pg, 134 const struct TALER_ReservePublicKeyP *reserve_pub) 135 { 136 struct GNUNET_PQ_QueryParam params[] = { 137 GNUNET_PQ_query_param_auto_from_type (reserve_pub), 138 GNUNET_PQ_query_param_end 139 }; 140 uint64_t row; 141 struct GNUNET_PQ_ResultSpec rs[] = { 142 GNUNET_PQ_result_spec_uint64 ("close_request_serial_id", 143 &row), 144 GNUNET_PQ_result_spec_end 145 }; 146 147 GNUNET_assert (GNUNET_OK == 148 GNUNET_PQ_prepare_anon (pg->conn, 149 "SELECT close_request_serial_id" 150 " FROM close_requests" 151 " WHERE reserve_pub=$1;")); 152 GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == 153 GNUNET_PQ_eval_prepared_singleton_select (pg->conn, 154 "", 155 params, 156 rs)); 157 return row; 158 } 159 160 161 /** 162 * Ask for a reserve to be closed. 163 * 164 * @param pg the database context 165 * @param reserve_pub reserve to close 166 * @param seed seed for the reserve signature 167 * @param when when the request was made, in seconds since the epoch 168 * @param balance balance to pay out, e.g. "10" 169 * @param fee closing fee, e.g. "0.5" 170 * @return transaction status 171 */ 172 static enum GNUNET_DB_QueryStatus 173 add_request (struct TALER_EXCHANGEDB_PostgresContext *pg, 174 const struct TALER_ReservePublicKeyP *reserve_pub, 175 uint32_t seed, 176 uint64_t when, 177 const char *balance, 178 const char *fee) 179 { 180 struct TALER_FullPayto payto = { 181 .full_payto = (char *) PAYTO 182 }; 183 struct TALER_ReserveSignatureP reserve_sig; 184 struct TALER_Amount b = TDB_amount (balance); 185 struct TALER_Amount f = TDB_amount (fee); 186 187 TDB_fill (&reserve_sig, 188 sizeof (reserve_sig), 189 seed); 190 return TALER_EXCHANGEDB_insert_close_request (pg, 191 reserve_pub, 192 payto, 193 &reserve_sig, 194 ts (when), 195 &b, 196 &f); 197 } 198 199 200 /** 201 * Nothing is known while the table is empty. 202 * 203 * @param pg the database context 204 * @return 0 on success 205 */ 206 static int 207 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg) 208 { 209 struct TALER_ReservePublicKeyP reserve_pub; 210 struct TALER_ReserveSignatureP reserve_sig; 211 struct GNUNET_TIME_Timestamp request_timestamp; 212 struct TALER_Amount close_balance; 213 struct TALER_Amount close_fee; 214 struct TALER_FullPayto payto = { NULL }; 215 struct ExpiredContext ctx = { 0 }; 216 217 TDB_FILL (reserve_pub, 218 1); 219 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 220 TALER_EXCHANGEDB_get_reserve_close_request_info (pg, 221 &reserve_pub, 222 1, 223 &reserve_sig, 224 &request_timestamp, 225 &close_balance, 226 &close_fee, 227 &payto)); 228 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 229 TALER_EXCHANGEDB_iterate_unfinished_close_requests (pg, 230 &expired_cb, 231 &ctx)); 232 FAILIF (0 != ctx.total); 233 return 0; 234 } 235 236 237 /** 238 * A close request is stored and found by (reserve, row). 239 * 240 * @param pg the database context 241 * @return 0 on success 242 */ 243 static int 244 check_insert_and_lookup (struct TALER_EXCHANGEDB_PostgresContext *pg) 245 { 246 struct TALER_ReservePublicKeyP reserve_pub; 247 struct TALER_ReserveSignatureP reserve_sig; 248 struct TALER_ReserveSignatureP expect_sig; 249 struct GNUNET_TIME_Timestamp request_timestamp; 250 struct TALER_Amount close_balance; 251 struct TALER_Amount close_fee; 252 struct TALER_Amount expect_balance = TDB_amount ("10"); 253 struct TALER_Amount expect_fee = TDB_amount ("0.5"); 254 struct TALER_FullPayto payto = { NULL }; 255 uint64_t row; 256 257 TDB_reserve (pg, 258 10, 259 "10", 260 &reserve_pub); 261 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 262 add_request (pg, 263 &reserve_pub, 264 10, 265 1600000000, 266 "10", 267 "0.5")); 268 row = request_row (pg, 269 &reserve_pub); 270 TDB_FILL (expect_sig, 271 10); 272 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 273 TALER_EXCHANGEDB_get_reserve_close_request_info (pg, 274 &reserve_pub, 275 row, 276 &reserve_sig, 277 &request_timestamp, 278 &close_balance, 279 &close_fee, 280 &payto)); 281 FAILIF (0 != GNUNET_memcmp (&reserve_sig, 282 &expect_sig)); 283 FAILIF (GNUNET_TIME_timestamp_cmp (request_timestamp, 284 !=, 285 ts (1600000000))); 286 FAILIF (0 != TALER_amount_cmp (&close_balance, 287 &expect_balance)); 288 FAILIF (0 != TALER_amount_cmp (&close_fee, 289 &expect_fee)); 290 FAILIF (NULL == payto.full_payto); 291 FAILIF_C (0 != strcmp (payto.full_payto, 292 PAYTO), 293 GNUNET_free (payto.full_payto)); 294 GNUNET_free (payto.full_payto); 295 296 /* the row has to belong to the reserve that is asked about */ 297 payto.full_payto = NULL; 298 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 299 TALER_EXCHANGEDB_get_reserve_close_request_info (pg, 300 &reserve_pub, 301 row + 1000, 302 &reserve_sig, 303 &request_timestamp, 304 &close_balance, 305 &close_fee, 306 &payto)); 307 { 308 struct TALER_ReservePublicKeyP other; 309 310 TDB_FILL (other, 311 99); 312 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 313 TALER_EXCHANGEDB_get_reserve_close_request_info (pg, 314 &other, 315 row, 316 &reserve_sig, 317 &request_timestamp, 318 &close_balance, 319 &close_fee, 320 &payto)); 321 } 322 323 /* a second request at the same timestamp is absorbed */ 324 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 325 add_request (pg, 326 &reserve_pub, 327 11, 328 1600000000, 329 "10", 330 "0.5")); 331 FAILIF (1 != TDB_count (pg, 332 "FROM close_requests")); 333 /* ...but one at another timestamp is a second request */ 334 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 335 add_request (pg, 336 &reserve_pub, 337 11, 338 1600003600, 339 "10", 340 "0.5")); 341 FAILIF (2 != TDB_count (pg, 342 "FROM close_requests")); 343 return 0; 344 } 345 346 347 /** 348 * The iterator hands over every unfinished request exactly once, marking 349 * it done as it goes. 350 * 351 * @param pg the database context 352 * @return 0 on success 353 */ 354 static int 355 check_iterate_unfinished (struct TALER_EXCHANGEDB_PostgresContext *pg) 356 { 357 struct ExpiredContext ctx; 358 359 FAILIF (2 != TDB_count (pg, 360 "FROM close_requests WHERE NOT done")); 361 memset (&ctx, 362 0, 363 sizeof (ctx)); 364 FAILIF (2 != 365 TALER_EXCHANGEDB_iterate_unfinished_close_requests (pg, 366 &expired_cb, 367 &ctx)); 368 FAILIF_C (2 != ctx.total, 369 GNUNET_free (ctx.account)); 370 FAILIF_C (NULL == ctx.account, 371 GNUNET_free (ctx.account)); 372 FAILIF_C (0 != strcmp (ctx.account, 373 PAYTO), 374 GNUNET_free (ctx.account)); 375 FAILIF_C (0 == ctx.close_request_row, 376 GNUNET_free (ctx.account)); 377 GNUNET_free (ctx.account); 378 FAILIF (0 != TDB_count (pg, 379 "FROM close_requests WHERE NOT done")); 380 381 /* everything is done now, so a second sweep finds nothing */ 382 memset (&ctx, 383 0, 384 sizeof (ctx)); 385 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 386 TALER_EXCHANGEDB_iterate_unfinished_close_requests (pg, 387 &expired_cb, 388 &ctx)); 389 FAILIF (0 != ctx.total); 390 391 /* a fresh request is picked up again */ 392 { 393 struct TALER_ReservePublicKeyP reserve_pub; 394 395 TDB_reserve (pg, 396 12, 397 "3", 398 &reserve_pub); 399 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 400 add_request (pg, 401 &reserve_pub, 402 12, 403 1600007200, 404 "3", 405 "0.5")); 406 memset (&ctx, 407 0, 408 sizeof (ctx)); 409 FAILIF (1 != 410 TALER_EXCHANGEDB_iterate_unfinished_close_requests (pg, 411 &expired_cb, 412 &ctx)); 413 FAILIF_C (0 != GNUNET_memcmp (&ctx.reserve_pub, 414 &reserve_pub), 415 GNUNET_free (ctx.account)); 416 GNUNET_free (ctx.account); 417 } 418 return 0; 419 } 420 421 422 /** 423 * The checks to run, in order. 424 */ 425 static const struct TDB_Test tests[] = { 426 { "close-requests-empty", 427 &check_empty }, 428 { "close-requests-insert-and-lookup", 429 &check_insert_and_lookup }, 430 { "close-requests-iterate-unfinished", 431 &check_iterate_unfinished }, 432 { NULL, NULL } 433 }; 434 435 436 int 437 main (int argc, 438 char *const *argv) 439 { 440 return TDB_main (argc, 441 argv, 442 "test-close-requests", 443 "Tests for the exchangedb `close_requests' table", 444 tests); 445 } 446 447 448 /* end of test_close_requests.c */