Line data Source code
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 20 : t0 (void)
54 : {
55 : static uint64_t cached;
56 :
57 20 : if (0 == cached)
58 1 : cached = GNUNET_TIME_absolute_get ().abs_value_us
59 1 : / (1000LLU * 1000LLU) - HOUR;
60 20 : 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 20 : ts (uint64_t secs)
78 : {
79 20 : struct GNUNET_TIME_Absolute abs = {
80 20 : .abs_value_us = secs * 1000LLU * 1000LLU
81 : };
82 :
83 20 : 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 3 : 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 3 : 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 3 : ctx->total++;
139 3 : ctx->value_sum += fees->history.value;
140 3 : ctx->purse_account_limit = purse_account_limit;
141 3 : }
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 3 : 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 3 : fees.history = TDB_amount (history);
167 3 : fees.account = TDB_amount ("0.5");
168 3 : fees.purse = TDB_amount ("0.25");
169 3 : TDB_FILL (master_sig,
170 : seed);
171 3 : 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 1 : 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 1 : struct FeeContext ctx = { 0 };
198 : uint32_t purse_account_limit;
199 :
200 1 : 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 1 : 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 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
219 : TALER_EXCHANGEDB_iterate_global_fees (pg,
220 : &global_fee_cb,
221 : &ctx));
222 1 : FAILIF (0 != ctx.total);
223 1 : 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 1 : 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 1 : struct TALER_Amount expect_history = TDB_amount ("1");
242 : struct TALER_MasterSignatureP master_sig;
243 : struct TALER_MasterSignatureP expect_sig;
244 1 : uint32_t purse_account_limit = 0;
245 :
246 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
247 : add_fee (pg,
248 : T0,
249 : T0 + HOUR,
250 : "1",
251 : 4,
252 : 20));
253 1 : TDB_FILL (expect_sig,
254 : 20);
255 1 : 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 1 : FAILIF (GNUNET_TIME_timestamp_cmp (start_date,
266 : !=,
267 : ts (T0)));
268 1 : FAILIF (GNUNET_TIME_timestamp_cmp (end_date,
269 : !=,
270 : ts (T0 + HOUR)));
271 1 : FAILIF (0 != TALER_amount_cmp (&fees.history,
272 : &expect_history));
273 1 : FAILIF (4 != purse_account_limit);
274 1 : FAILIF (GNUNET_TIME_relative_cmp (purse_timeout,
275 : !=,
276 : GNUNET_TIME_UNIT_HOURS));
277 1 : FAILIF (GNUNET_TIME_relative_cmp (history_expiration,
278 : !=,
279 : GNUNET_TIME_UNIT_DAYS));
280 1 : FAILIF (0 != GNUNET_memcmp (&master_sig,
281 : &expect_sig));
282 :
283 : /* the interval is half-open */
284 1 : 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 1 : 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 1 : 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 1 : 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 1 : struct TALER_Amount expect_history = TDB_amount ("1");
322 : struct FeeContext ctx;
323 : uint32_t purse_account_limit;
324 :
325 1 : 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 1 : 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 1 : memset (&fees,
343 : 0,
344 : sizeof (fees));
345 1 : 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 1 : FAILIF (0 != TALER_amount_cmp (&fees.history,
354 : &expect_history));
355 1 : FAILIF (4 != purse_account_limit);
356 :
357 1 : memset (&fees,
358 : 0,
359 : sizeof (fees));
360 1 : 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 1 : FAILIF (GNUNET_OK ==
369 : TALER_amount_is_valid (&fees.history));
370 :
371 1 : 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 1 : memset (&ctx,
381 : 0,
382 : sizeof (ctx));
383 1 : FAILIF (0 >=
384 : TALER_EXCHANGEDB_iterate_global_fees (pg,
385 : &global_fee_cb,
386 : &ctx));
387 1 : FAILIF (3 != ctx.total);
388 1 : FAILIF (1 + 1 + 9 != ctx.value_sum);
389 1 : 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 1 : main (int argc,
409 : char *const *argv)
410 : {
411 1 : 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 */
|