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_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 5 : ts (uint64_t secs)
52 : {
53 5 : struct GNUNET_TIME_Absolute abs = {
54 5 : .abs_value_us = secs * 1000LLU * 1000LLU
55 : };
56 :
57 5 : 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 1 : 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 1 : struct DecisionContext *ctx = cls;
117 :
118 : (void) rowid;
119 : (void) purse_value;
120 1 : ctx->total++;
121 1 : if ( (NULL != ctx->purse_pub) &&
122 1 : (0 == GNUNET_memcmp (purse_pub,
123 : ctx->purse_pub)) )
124 : {
125 1 : ctx->matched++;
126 1 : ctx->have_reserve = (NULL != reserve_pub);
127 : }
128 1 : if ( (0 != ctx->stop_after) &&
129 0 : (ctx->total >= ctx->stop_after) )
130 0 : return GNUNET_SYSERR;
131 1 : 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 2 : all_decision_cb (void *cls,
147 : uint64_t rowid,
148 : const struct TALER_PurseContractPublicKeyP *purse_pub,
149 : bool refunded)
150 : {
151 2 : struct DecisionContext *ctx = cls;
152 :
153 : (void) rowid;
154 2 : ctx->total++;
155 2 : if ( (NULL != ctx->purse_pub) &&
156 1 : (0 == GNUNET_memcmp (purse_pub,
157 : ctx->purse_pub)) )
158 : {
159 1 : ctx->matched++;
160 1 : ctx->refunded = refunded;
161 : }
162 2 : if ( (0 != ctx->stop_after) &&
163 1 : (ctx->total >= ctx->stop_after) )
164 1 : return GNUNET_SYSERR;
165 1 : 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 1 : check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
177 : {
178 1 : struct DecisionContext ctx = { 0 };
179 : struct GNUNET_TIME_Absolute expiration;
180 :
181 1 : TDB_denom (pg,
182 : 10,
183 : "5",
184 : "0.1",
185 : &denom);
186 1 : 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 1 : 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 1 : 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 1 : FAILIF (0 != ctx.total);
205 1 : 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 1 : 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 1 : TDB_purse (pg,
223 : 10,
224 : "5",
225 : ts (1700000000),
226 : &purse);
227 : /* ...but we only look at purses that expired before 2020 */
228 1 : 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 1 : FAILIF (0 != TDB_count (pg,
234 : "FROM purse_decision"));
235 1 : 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 1 : 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 1 : 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 1 : TDB_purse (pg,
262 : 11,
263 : "5",
264 : ts (1600000000),
265 : &purse);
266 1 : TDB_coin (pg,
267 : &denom,
268 : 20,
269 : &coin,
270 : NULL);
271 1 : TDB_FILL (coin_sig,
272 : 20);
273 1 : 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 1 : FAILIF_C (! balance_ok,
285 : TDB_coin_free (&coin));
286 1 : hex = TDB_hex (&coin.coin_pub,
287 : sizeof (coin.coin_pub));
288 1 : 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 1 : 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 1 : FAILIF_C (expiration.abs_value_us !=
302 : ts (1600000000).abs_time.abs_value_us,
303 : GNUNET_free (hex); TDB_coin_free (&coin));
304 1 : 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 1 : 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 1 : GNUNET_free (hex);
315 1 : TDB_coin_free (&coin);
316 :
317 : /* the decision is a refund, so the by-refund iterator sees it... */
318 1 : memset (&ctx,
319 : 0,
320 : sizeof (ctx));
321 1 : ctx.purse_pub = &purse.purse_pub;
322 1 : FAILIF (0 >=
323 : TALER_EXCHANGEDB_iterate_purse_decisions_above_serial_id (
324 : pg,
325 : 0,
326 : true,
327 : &decision_cb,
328 : &ctx));
329 1 : FAILIF (1 != ctx.matched);
330 : /* ...a refunded purse has no reserve to credit */
331 1 : FAILIF (ctx.have_reserve);
332 :
333 : /* ...and the non-refunded view does not */
334 1 : memset (&ctx,
335 : 0,
336 : sizeof (ctx));
337 1 : ctx.purse_pub = &purse.purse_pub;
338 1 : 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 1 : FAILIF (0 != ctx.matched);
346 :
347 : /* the all-decisions view reports it with its refund flag */
348 1 : memset (&ctx,
349 : 0,
350 : sizeof (ctx));
351 1 : ctx.purse_pub = &purse.purse_pub;
352 1 : FAILIF (0 >=
353 : TALER_EXCHANGEDB_iterate_all_purse_decisions_above_serial_id (
354 : pg,
355 : 0,
356 : &all_decision_cb,
357 : &ctx));
358 1 : FAILIF (1 != ctx.matched);
359 1 : FAILIF (! ctx.refunded);
360 :
361 : /* a second sweep finds nothing left to expire */
362 1 : 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 1 : FAILIF (1 != TDB_count (pg,
368 : "FROM purse_decision"));
369 1 : 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 1 : check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
381 : {
382 : struct DecisionContext ctx;
383 :
384 1 : memset (&ctx,
385 : 0,
386 : sizeof (ctx));
387 1 : 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 1 : FAILIF (0 != ctx.total);
394 1 : memset (&ctx,
395 : 0,
396 : sizeof (ctx));
397 1 : ctx.stop_after = 1;
398 1 : FAILIF (1 !=
399 : TALER_EXCHANGEDB_iterate_all_purse_decisions_above_serial_id (
400 : pg,
401 : 0,
402 : &all_decision_cb,
403 : &ctx));
404 1 : FAILIF (1 != ctx.total);
405 1 : 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 1 : main (int argc,
427 : char *const *argv)
428 : {
429 : int ret;
430 :
431 1 : ret = TDB_main (argc,
432 : argv,
433 : "test-purse-decision",
434 : "Tests for the exchangedb `purse_decision' table",
435 : tests);
436 1 : TDB_denom_free (&denom);
437 1 : return ret;
438 : }
439 :
440 :
441 : /* end of test_purse_decision.c */
|