test_reserve_history.c (13865B)
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_reserve_history.c 18 * @brief tests for the exchangedb functions whose primary table is 19 * `reserve_history` 20 * @author Christian Grothoff 21 * 22 * Covers #TALER_EXCHANGEDB_get_reserve_history(). 23 * 24 * `reserve_history` is an index: AFTER INSERT triggers on the tables that 25 * change a reserve add one row each, naming the table and the row. The 26 * function walks that index newest-first and looks each entry up in the 27 * table it names. So the checks build a reserve with several kinds of 28 * operation on it and read the history back, and they check the ETag 29 * short-circuit -- which must not leave a transaction open. 30 */ 31 #include "test_common.h" 32 #include "exchange-database/get_reserve_history.h" 33 #include "exchange-database/insert_close_request.h" 34 #include "exchange-database/start.h" 35 #include "exchange-database/rollback.h" 36 37 38 /** 39 * Account the checks use. 40 */ 41 static struct TDB_Account account; 42 43 44 /** 45 * Count the entries of @a rh, and how many of them have type @a type. 46 * 47 * @param rh history to walk 48 * @param type operation to count 49 * @param[out] matched set to the number of entries of that type 50 * @return total number of entries 51 */ 52 static unsigned int 53 count_history (const struct TALER_EXCHANGEDB_ReserveHistory *rh, 54 enum TALER_EXCHANGEDB_ReserveOperation type, 55 unsigned int *matched) 56 { 57 unsigned int total = 0; 58 59 *matched = 0; 60 for (const struct TALER_EXCHANGEDB_ReserveHistory *p = rh; 61 NULL != p; 62 p = p->next) 63 { 64 total++; 65 if (type == p->type) 66 (*matched)++; 67 } 68 return total; 69 } 70 71 72 /** 73 * An unknown reserve has no history at all. 74 * 75 * @param pg the database context 76 * @return 0 on success 77 */ 78 static int 79 check_unknown_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg) 80 { 81 struct TALER_ReservePublicKeyP reserve_pub; 82 struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL; 83 struct TALER_Amount balance; 84 uint64_t etag_out = 42; 85 86 TDB_FILL (reserve_pub, 87 1); 88 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 89 TALER_EXCHANGEDB_get_reserve_history (pg, 90 &reserve_pub, 91 0, 92 0, 93 &etag_out, 94 &balance, 95 &rh)); 96 FAILIF (NULL != rh); 97 return 0; 98 } 99 100 101 /** 102 * A reserve that exists but was never touched has no history either: the 103 * `reserves` row alone does not create an index entry. 104 * 105 * @param pg the database context 106 * @return 0 on success 107 */ 108 static int 109 check_untouched_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg) 110 { 111 struct TALER_ReservePublicKeyP reserve_pub; 112 struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL; 113 struct TALER_Amount balance; 114 uint64_t etag_out = 42; 115 116 TDB_reserve (pg, 117 2, 118 "5", 119 &reserve_pub); 120 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 121 TALER_EXCHANGEDB_get_reserve_history (pg, 122 &reserve_pub, 123 0, 124 0, 125 &etag_out, 126 &balance, 127 &rh)); 128 FAILIF (NULL != rh); 129 return 0; 130 } 131 132 133 /** 134 * A bank transfer into the reserve shows up as one history entry. 135 * 136 * @param pg the database context 137 * @return 0 on success 138 */ 139 static int 140 check_bank_transfer (struct TALER_EXCHANGEDB_PostgresContext *pg) 141 { 142 struct TALER_ReservePublicKeyP reserve_pub; 143 struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL; 144 struct TALER_Amount balance; 145 struct TALER_Amount expect = TDB_amount ("10"); 146 unsigned int matched; 147 uint64_t etag_out = 0; 148 149 TDB_account (pg, 150 10, 151 &account); 152 TDB_reserve_in (pg, 153 &account, 154 10, 155 "10", 156 &reserve_pub); 157 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 158 TALER_EXCHANGEDB_get_reserve_history (pg, 159 &reserve_pub, 160 0, 161 0, 162 &etag_out, 163 &balance, 164 &rh)); 165 FAILIF_C (NULL == rh, 166 TALER_EXCHANGEDB_free_reserve_history (rh)); 167 FAILIF_C (0 == etag_out, 168 TALER_EXCHANGEDB_free_reserve_history (rh)); 169 FAILIF_C (0 != TALER_amount_cmp (&balance, 170 &expect), 171 TALER_EXCHANGEDB_free_reserve_history (rh)); 172 FAILIF_C (1 != count_history (rh, 173 TALER_EXCHANGEDB_RO_BANK_TO_EXCHANGE, 174 &matched), 175 TALER_EXCHANGEDB_free_reserve_history (rh)); 176 FAILIF_C (1 != matched, 177 TALER_EXCHANGEDB_free_reserve_history (rh)); 178 FAILIF_C (0 != TALER_amount_cmp (&rh->details.bank->amount, 179 &expect), 180 TALER_EXCHANGEDB_free_reserve_history (rh)); 181 FAILIF_C (0 != strcmp (rh->details.bank->sender_account_details.full_payto, 182 account.payto.full_payto), 183 TALER_EXCHANGEDB_free_reserve_history (rh)); 184 TALER_EXCHANGEDB_free_reserve_history (rh); 185 return 0; 186 } 187 188 189 /** 190 * A close request adds a second entry, and the offset filter hides the 191 * older one. 192 * 193 * @param pg the database context 194 * @return 0 on success 195 */ 196 static int 197 check_two_entries (struct TALER_EXCHANGEDB_PostgresContext *pg) 198 { 199 struct TALER_ReservePublicKeyP reserve_pub; 200 struct TALER_ReserveSignatureP reserve_sig; 201 struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL; 202 struct TALER_Amount balance; 203 struct TALER_Amount close_balance = TDB_amount ("10"); 204 struct TALER_Amount close_fee = TDB_amount ("0.5"); 205 unsigned int matched; 206 uint64_t etag_first; 207 uint64_t etag_out = 0; 208 uint64_t first_off; 209 210 TDB_fill (&reserve_pub, 211 sizeof (reserve_pub), 212 10); 213 TDB_FILL (reserve_sig, 214 10); 215 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 216 TALER_EXCHANGEDB_get_reserve_history (pg, 217 &reserve_pub, 218 0, 219 0, 220 &etag_first, 221 &balance, 222 &rh)); 223 first_off = rh->history_offset; 224 TALER_EXCHANGEDB_free_reserve_history (rh); 225 rh = NULL; 226 227 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 228 TALER_EXCHANGEDB_insert_close_request (pg, 229 &reserve_pub, 230 account.payto, 231 &reserve_sig, 232 GNUNET_TIME_timestamp_get (), 233 &close_balance, 234 &close_fee)); 235 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 236 TALER_EXCHANGEDB_get_reserve_history (pg, 237 &reserve_pub, 238 0, 239 0, 240 &etag_out, 241 &balance, 242 &rh)); 243 FAILIF_C (2 != count_history (rh, 244 TALER_EXCHANGEDB_RO_CLOSE_REQUEST, 245 &matched), 246 TALER_EXCHANGEDB_free_reserve_history (rh)); 247 FAILIF_C (1 != matched, 248 TALER_EXCHANGEDB_free_reserve_history (rh)); 249 /* The query runs newest-first but the rows are walked back to front, so 250 the list that comes out is oldest-first. */ 251 FAILIF_C (TALER_EXCHANGEDB_RO_BANK_TO_EXCHANGE != rh->type, 252 TALER_EXCHANGEDB_free_reserve_history (rh)); 253 FAILIF_C (TALER_EXCHANGEDB_RO_CLOSE_REQUEST != rh->next->type, 254 TALER_EXCHANGEDB_free_reserve_history (rh)); 255 FAILIF_C (rh->history_offset >= rh->next->history_offset, 256 TALER_EXCHANGEDB_free_reserve_history (rh)); 257 FAILIF_C (etag_out <= etag_first, 258 TALER_EXCHANGEDB_free_reserve_history (rh)); 259 TALER_EXCHANGEDB_free_reserve_history (rh); 260 rh = NULL; 261 262 /* starting past the bank transfer leaves only the close request */ 263 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 264 TALER_EXCHANGEDB_get_reserve_history (pg, 265 &reserve_pub, 266 first_off, 267 0, 268 &etag_out, 269 &balance, 270 &rh)); 271 FAILIF_C (1 != count_history (rh, 272 TALER_EXCHANGEDB_RO_CLOSE_REQUEST, 273 &matched), 274 TALER_EXCHANGEDB_free_reserve_history (rh)); 275 FAILIF_C (1 != matched, 276 TALER_EXCHANGEDB_free_reserve_history (rh)); 277 TALER_EXCHANGEDB_free_reserve_history (rh); 278 rh = NULL; 279 280 /* starting past everything leaves an empty history, but the reserve is 281 still known and the ETag is still reported */ 282 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 283 TALER_EXCHANGEDB_get_reserve_history (pg, 284 &reserve_pub, 285 etag_out, 286 0, 287 &etag_out, 288 &balance, 289 &rh)); 290 FAILIF_C (NULL != rh, 291 TALER_EXCHANGEDB_free_reserve_history (rh)); 292 return 0; 293 } 294 295 296 /** 297 * Passing the current ETag short-circuits the lookup -- and must not leave 298 * the transaction it opened behind. 299 * 300 * @param pg the database context 301 * @return 0 on success 302 */ 303 static int 304 check_etag (struct TALER_EXCHANGEDB_PostgresContext *pg) 305 { 306 struct TALER_ReservePublicKeyP reserve_pub; 307 struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL; 308 struct TALER_Amount balance; 309 uint64_t etag_out = 0; 310 uint64_t etag_again = 0; 311 312 TDB_fill (&reserve_pub, 313 sizeof (reserve_pub), 314 10); 315 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 316 TALER_EXCHANGEDB_get_reserve_history (pg, 317 &reserve_pub, 318 0, 319 0, 320 &etag_out, 321 &balance, 322 &rh)); 323 TALER_EXCHANGEDB_free_reserve_history (rh); 324 rh = NULL; 325 326 /* the client already has this version */ 327 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 328 TALER_EXCHANGEDB_get_reserve_history (pg, 329 &reserve_pub, 330 0, 331 etag_out, 332 &etag_again, 333 &balance, 334 &rh)); 335 FAILIF (etag_again != etag_out); 336 FAILIF (NULL != rh); 337 338 /* The short-circuit above returns early. If it forgets to close the 339 read-committed transaction it opened, the connection is parked in 340 "idle in transaction" -- which the next TALER_EXCHANGEDB_start() would 341 paper over, since its preflight rolls the stale transaction back and 342 only logs it. So check the bookkeeping preflight itself looks at. */ 343 FAILIF (NULL != pg->transaction_name); 344 FAILIF (GNUNET_OK != 345 TALER_EXCHANGEDB_start (pg, 346 "test-reserve-history-etag")); 347 FAILIF (NULL == pg->transaction_name); 348 TALER_EXCHANGEDB_rollback (pg); 349 FAILIF (NULL != pg->transaction_name); 350 return 0; 351 } 352 353 354 /** 355 * The checks to run, in order. 356 */ 357 static const struct TDB_Test tests[] = { 358 { "reserve-history-unknown-reserve", 359 &check_unknown_reserve }, 360 { "reserve-history-untouched-reserve", 361 &check_untouched_reserve }, 362 { "reserve-history-bank-transfer", 363 &check_bank_transfer }, 364 { "reserve-history-two-entries", 365 &check_two_entries }, 366 { "reserve-history-etag", 367 &check_etag }, 368 { NULL, NULL } 369 }; 370 371 372 int 373 main (int argc, 374 char *const *argv) 375 { 376 int ret; 377 378 ret = TDB_main (argc, 379 argv, 380 "test-reserve-history", 381 "Tests for the exchangedb `reserve_history' table", 382 tests); 383 TDB_account_free (&account); 384 return ret; 385 } 386 387 388 /* end of test_reserve_history.c */