test_global_fee.c (14068B)
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_global_fee.c 18 * @brief tests for the exchangedb functions whose primary table is 19 * `global_fee` 20 * @author Christian Grothoff 21 * 22 * Covers #TALER_EXCHANGEDB_insert_global_fee(), 23 * #TALER_EXCHANGEDB_get_global_fee(), 24 * #TALER_EXCHANGEDB_get_global_fee_by_time() and 25 * #TALER_EXCHANGEDB_iterate_global_fees(). 26 * 27 * `global_fee` has no foreign keys and, unlike `wire_fee`, no wire method 28 * to key on -- the validity interval is all there is. As with the wire 29 * fees, the by-time lookup returns an invalid amount when the range covers 30 * two different fee sets. 31 */ 32 #include "test_common.h" 33 #include "exchange-database/insert_global_fee.h" 34 #include "exchange-database/get_global_fee.h" 35 #include "exchange-database/get_global_fee_by_time.h" 36 #include "exchange-database/iterate_global_fees.h" 37 38 39 /** 40 * One hour, in seconds. 41 */ 42 #define HOUR 3600 43 44 45 /** 46 * Start of the first fee period, in seconds since the epoch. It has to be 47 * recent: #TALER_EXCHANGEDB_iterate_global_fees() only reports periods 48 * that started within the last year. 49 * 50 * @return an hour ago, rounded down to a whole second 51 */ 52 static uint64_t 53 t0 (void) 54 { 55 static uint64_t cached; 56 57 if (0 == cached) 58 cached = GNUNET_TIME_absolute_get ().abs_value_us 59 / (1000LLU * 1000LLU) - HOUR; 60 return cached; 61 } 62 63 64 /** 65 * Convenience alias for the start of the first fee period. 66 */ 67 #define T0 (t0 ()) 68 69 70 /** 71 * Build a timestamp from a number of seconds since the epoch. 72 * 73 * @param secs seconds since the epoch 74 * @return the timestamp 75 */ 76 static struct GNUNET_TIME_Timestamp 77 ts (uint64_t secs) 78 { 79 struct GNUNET_TIME_Absolute abs = { 80 .abs_value_us = secs * 1000LLU * 1000LLU 81 }; 82 83 return GNUNET_TIME_absolute_to_timestamp (abs); 84 } 85 86 87 /** 88 * Closure for #global_fee_cb(). 89 */ 90 struct FeeContext 91 { 92 /** 93 * How many periods did the callback see? 94 */ 95 unsigned int total; 96 97 /** 98 * Sum of the whole-unit parts of the history fees seen. 99 */ 100 uint64_t value_sum; 101 102 /** 103 * Purse account limit of the last period seen. 104 */ 105 uint32_t purse_account_limit; 106 }; 107 108 109 /** 110 * Callback for #TALER_EXCHANGEDB_iterate_global_fees(). 111 * 112 * @param cls a `struct FeeContext *` 113 * @param fees the fees of this period 114 * @param purse_timeout when purses time out 115 * @param history_expiration how long histories are kept 116 * @param purse_account_limit free purses per account 117 * @param start_date start of the period 118 * @param end_date end of the period 119 * @param master_sig signature over the period 120 */ 121 static void 122 global_fee_cb (void *cls, 123 const struct TALER_GlobalFeeSet *fees, 124 struct GNUNET_TIME_Relative purse_timeout, 125 struct GNUNET_TIME_Relative history_expiration, 126 uint32_t purse_account_limit, 127 struct GNUNET_TIME_Timestamp start_date, 128 struct GNUNET_TIME_Timestamp end_date, 129 const struct TALER_MasterSignatureP *master_sig) 130 { 131 struct FeeContext *ctx = cls; 132 133 (void) purse_timeout; 134 (void) history_expiration; 135 (void) start_date; 136 (void) end_date; 137 (void) master_sig; 138 ctx->total++; 139 ctx->value_sum += fees->history.value; 140 ctx->purse_account_limit = purse_account_limit; 141 } 142 143 144 /** 145 * Insert a fee period. 146 * 147 * @param pg the database context 148 * @param from start of the period 149 * @param until end of the period 150 * @param history history fee, e.g. "1" 151 * @param limit free purses per account 152 * @param seed seed for the master signature 153 * @return transaction status 154 */ 155 static enum GNUNET_DB_QueryStatus 156 add_fee (struct TALER_EXCHANGEDB_PostgresContext *pg, 157 uint64_t from, 158 uint64_t until, 159 const char *history, 160 uint32_t limit, 161 uint32_t seed) 162 { 163 struct TALER_GlobalFeeSet fees; 164 struct TALER_MasterSignatureP master_sig; 165 166 fees.history = TDB_amount (history); 167 fees.account = TDB_amount ("0.5"); 168 fees.purse = TDB_amount ("0.25"); 169 TDB_FILL (master_sig, 170 seed); 171 return TALER_EXCHANGEDB_insert_global_fee (pg, 172 ts (from), 173 ts (until), 174 &fees, 175 GNUNET_TIME_UNIT_HOURS, 176 GNUNET_TIME_UNIT_DAYS, 177 limit, 178 &master_sig); 179 } 180 181 182 /** 183 * No fees means no lookups succeed and the iterator sees nothing. 184 * 185 * @param pg the database context 186 * @return 0 on success 187 */ 188 static int 189 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg) 190 { 191 struct GNUNET_TIME_Timestamp start_date; 192 struct GNUNET_TIME_Timestamp end_date; 193 struct GNUNET_TIME_Relative purse_timeout; 194 struct GNUNET_TIME_Relative history_expiration; 195 struct TALER_GlobalFeeSet fees; 196 struct TALER_MasterSignatureP master_sig; 197 struct FeeContext ctx = { 0 }; 198 uint32_t purse_account_limit; 199 200 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 201 TALER_EXCHANGEDB_get_global_fee (pg, 202 ts (T0), 203 &start_date, 204 &end_date, 205 &fees, 206 &purse_timeout, 207 &history_expiration, 208 &purse_account_limit, 209 &master_sig)); 210 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 211 TALER_EXCHANGEDB_get_global_fee_by_time (pg, 212 ts (T0), 213 ts (T0 + HOUR), 214 &fees, 215 &purse_timeout, 216 &history_expiration, 217 &purse_account_limit)); 218 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 219 TALER_EXCHANGEDB_iterate_global_fees (pg, 220 &global_fee_cb, 221 &ctx)); 222 FAILIF (0 != ctx.total); 223 return 0; 224 } 225 226 227 /** 228 * A single fee period is found inside its interval and nowhere else. 229 * 230 * @param pg the database context 231 * @return 0 on success 232 */ 233 static int 234 check_single_period (struct TALER_EXCHANGEDB_PostgresContext *pg) 235 { 236 struct GNUNET_TIME_Timestamp start_date; 237 struct GNUNET_TIME_Timestamp end_date; 238 struct GNUNET_TIME_Relative purse_timeout; 239 struct GNUNET_TIME_Relative history_expiration; 240 struct TALER_GlobalFeeSet fees; 241 struct TALER_Amount expect_history = TDB_amount ("1"); 242 struct TALER_MasterSignatureP master_sig; 243 struct TALER_MasterSignatureP expect_sig; 244 uint32_t purse_account_limit = 0; 245 246 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 247 add_fee (pg, 248 T0, 249 T0 + HOUR, 250 "1", 251 4, 252 20)); 253 TDB_FILL (expect_sig, 254 20); 255 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 256 TALER_EXCHANGEDB_get_global_fee (pg, 257 ts (T0), 258 &start_date, 259 &end_date, 260 &fees, 261 &purse_timeout, 262 &history_expiration, 263 &purse_account_limit, 264 &master_sig)); 265 FAILIF (GNUNET_TIME_timestamp_cmp (start_date, 266 !=, 267 ts (T0))); 268 FAILIF (GNUNET_TIME_timestamp_cmp (end_date, 269 !=, 270 ts (T0 + HOUR))); 271 FAILIF (0 != TALER_amount_cmp (&fees.history, 272 &expect_history)); 273 FAILIF (4 != purse_account_limit); 274 FAILIF (GNUNET_TIME_relative_cmp (purse_timeout, 275 !=, 276 GNUNET_TIME_UNIT_HOURS)); 277 FAILIF (GNUNET_TIME_relative_cmp (history_expiration, 278 !=, 279 GNUNET_TIME_UNIT_DAYS)); 280 FAILIF (0 != GNUNET_memcmp (&master_sig, 281 &expect_sig)); 282 283 /* the interval is half-open */ 284 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 285 TALER_EXCHANGEDB_get_global_fee (pg, 286 ts (T0 + HOUR), 287 &start_date, 288 &end_date, 289 &fees, 290 &purse_timeout, 291 &history_expiration, 292 &purse_account_limit, 293 &master_sig)); 294 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 295 TALER_EXCHANGEDB_get_global_fee (pg, 296 ts (T0 - 1), 297 &start_date, 298 &end_date, 299 &fees, 300 &purse_timeout, 301 &history_expiration, 302 &purse_account_limit, 303 &master_sig)); 304 return 0; 305 } 306 307 308 /** 309 * Over a range with one fee set, get_global_fee_by_time() returns it; over 310 * a range spanning two different ones it returns an invalid amount. 311 * 312 * @param pg the database context 313 * @return 0 on success 314 */ 315 static int 316 check_by_time (struct TALER_EXCHANGEDB_PostgresContext *pg) 317 { 318 struct GNUNET_TIME_Relative purse_timeout; 319 struct GNUNET_TIME_Relative history_expiration; 320 struct TALER_GlobalFeeSet fees; 321 struct TALER_Amount expect_history = TDB_amount ("1"); 322 struct FeeContext ctx; 323 uint32_t purse_account_limit; 324 325 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 326 add_fee (pg, 327 T0 + HOUR, 328 T0 + 2 * HOUR, 329 "1", 330 4, 331 21)); 332 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 333 add_fee (pg, 334 T0 + 2 * HOUR, 335 T0 + 3 * HOUR, 336 "9", 337 4, 338 22)); 339 340 /* Two periods that agree: still the fee. The status is the number of 341 rows the range covered, so it is 2 rather than ONE_RESULT. */ 342 memset (&fees, 343 0, 344 sizeof (fees)); 345 FAILIF (2 != 346 TALER_EXCHANGEDB_get_global_fee_by_time (pg, 347 ts (T0), 348 ts (T0 + 2 * HOUR), 349 &fees, 350 &purse_timeout, 351 &history_expiration, 352 &purse_account_limit)); 353 FAILIF (0 != TALER_amount_cmp (&fees.history, 354 &expect_history)); 355 FAILIF (4 != purse_account_limit); 356 357 memset (&fees, 358 0, 359 sizeof (fees)); 360 FAILIF (0 > 361 TALER_EXCHANGEDB_get_global_fee_by_time (pg, 362 ts (T0), 363 ts (T0 + 3 * HOUR), 364 &fees, 365 &purse_timeout, 366 &history_expiration, 367 &purse_account_limit)); 368 FAILIF (GNUNET_OK == 369 TALER_amount_is_valid (&fees.history)); 370 371 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 372 TALER_EXCHANGEDB_get_global_fee_by_time (pg, 373 ts (T0 + 10 * HOUR), 374 ts (T0 + 11 * HOUR), 375 &fees, 376 &purse_timeout, 377 &history_expiration, 378 &purse_account_limit)); 379 380 memset (&ctx, 381 0, 382 sizeof (ctx)); 383 FAILIF (0 >= 384 TALER_EXCHANGEDB_iterate_global_fees (pg, 385 &global_fee_cb, 386 &ctx)); 387 FAILIF (3 != ctx.total); 388 FAILIF (1 + 1 + 9 != ctx.value_sum); 389 return 0; 390 } 391 392 393 /** 394 * The checks to run, in order. 395 */ 396 static const struct TDB_Test tests[] = { 397 { "global-fee-empty", 398 &check_empty }, 399 { "global-fee-single-period", 400 &check_single_period }, 401 { "global-fee-by-time", 402 &check_by_time }, 403 { NULL, NULL } 404 }; 405 406 407 int 408 main (int argc, 409 char *const *argv) 410 { 411 return TDB_main (argc, 412 argv, 413 "test-global-fee", 414 "Tests for the exchangedb `global_fee' table", 415 tests); 416 } 417 418 419 /* end of test_global_fee.c */