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_deletion.c
18 : * @brief tests for the exchangedb functions whose primary table is
19 : * `purse_deletion`
20 : * @author Christian Grothoff
21 : *
22 : * Covers #TALER_EXCHANGEDB_do_purse_delete() and
23 : * #TALER_EXCHANGEDB_iterate_all_purse_deletions_above_serial_id().
24 : *
25 : * Deleting a purse is only possible while it is undecided: the two
26 : * negative answers of do_purse_delete() are "no such purse" and "already
27 : * decided", and the checks below produce both.
28 : */
29 : #include "test_common.h"
30 : #include "exchange-database/do_expire_purse.h"
31 : #include "exchange-database/do_purse_delete.h"
32 : #include "exchange-database/do_purse_deposit.h"
33 : #include "exchange-database/get_purse.h"
34 : #include "exchange-database/iterate_all_purse_deletions_above_serial_id.h"
35 :
36 :
37 : /**
38 : * Denomination the checks deposit into purses.
39 : */
40 : static struct TDB_Denom denom;
41 :
42 :
43 : /**
44 : * Build a timestamp from a number of seconds since the epoch.
45 : *
46 : * @param secs seconds since the epoch
47 : * @return the timestamp
48 : */
49 : static struct GNUNET_TIME_Timestamp
50 2 : ts (uint64_t secs)
51 : {
52 2 : struct GNUNET_TIME_Absolute abs = {
53 2 : .abs_value_us = secs * 1000LLU * 1000LLU
54 : };
55 :
56 2 : return GNUNET_TIME_absolute_to_timestamp (abs);
57 : }
58 :
59 :
60 : /**
61 : * Closure for #deletion_cb().
62 : */
63 : struct DeletionContext
64 : {
65 : /**
66 : * How many rows did the callback see?
67 : */
68 : unsigned int total;
69 :
70 : /**
71 : * Stop after this many rows; 0 for no limit.
72 : */
73 : unsigned int stop_after;
74 :
75 : /**
76 : * Purse we are looking for, NULL to match nothing.
77 : */
78 : const struct TALER_PurseContractPublicKeyP *purse_pub;
79 :
80 : /**
81 : * How many times did we see it?
82 : */
83 : unsigned int matched;
84 :
85 : /**
86 : * Signature reported for it.
87 : */
88 : struct TALER_PurseContractSignatureP purse_sig;
89 : };
90 :
91 :
92 : /**
93 : * Callback for
94 : * #TALER_EXCHANGEDB_iterate_all_purse_deletions_above_serial_id().
95 : *
96 : * @param cls a `struct DeletionContext *`
97 : * @param rowid row of the deletion
98 : * @param purse_pub the purse that was deleted
99 : * @param purse_sig signature affirming the deletion
100 : * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
101 : */
102 : static enum GNUNET_GenericReturnValue
103 2 : deletion_cb (void *cls,
104 : uint64_t rowid,
105 : const struct TALER_PurseContractPublicKeyP *purse_pub,
106 : const struct TALER_PurseContractSignatureP *purse_sig)
107 : {
108 2 : struct DeletionContext *ctx = cls;
109 :
110 : (void) rowid;
111 2 : ctx->total++;
112 2 : if ( (NULL != ctx->purse_pub) &&
113 1 : (0 == GNUNET_memcmp (purse_pub,
114 : ctx->purse_pub)) )
115 : {
116 1 : ctx->matched++;
117 1 : ctx->purse_sig = *purse_sig;
118 : }
119 2 : if ( (0 != ctx->stop_after) &&
120 1 : (ctx->total >= ctx->stop_after) )
121 1 : return GNUNET_SYSERR;
122 1 : return GNUNET_OK;
123 : }
124 :
125 :
126 : /**
127 : * A purse that does not exist cannot be deleted.
128 : *
129 : * @param pg the database context
130 : * @return 0 on success
131 : */
132 : static int
133 1 : check_unknown_purse (struct TALER_EXCHANGEDB_PostgresContext *pg)
134 : {
135 : struct TALER_PurseContractPublicKeyP purse_pub;
136 : struct TALER_PurseContractSignatureP purse_sig;
137 1 : struct DeletionContext ctx = { 0 };
138 1 : bool decided = true;
139 1 : bool found = true;
140 :
141 1 : TDB_denom (pg,
142 : 10,
143 : "5",
144 : "0.1",
145 : &denom);
146 1 : TDB_FILL (purse_pub,
147 : 1);
148 1 : TDB_FILL (purse_sig,
149 : 1);
150 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
151 : TALER_EXCHANGEDB_do_purse_delete (pg,
152 : &purse_pub,
153 : &purse_sig,
154 : &decided,
155 : &found));
156 1 : FAILIF (found);
157 1 : FAILIF (0 != TDB_count (pg,
158 : "FROM purse_deletion"));
159 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
160 : TALER_EXCHANGEDB_iterate_all_purse_deletions_above_serial_id (
161 : pg,
162 : 0,
163 : &deletion_cb,
164 : &ctx));
165 1 : FAILIF (0 != ctx.total);
166 1 : return 0;
167 : }
168 :
169 :
170 : /**
171 : * Deleting an undecided purse records the deletion, refunds the coins in
172 : * it and shows up in the purse's status.
173 : *
174 : * @param pg the database context
175 : * @return 0 on success
176 : */
177 : static int
178 1 : check_delete (struct TALER_EXCHANGEDB_PostgresContext *pg)
179 : {
180 : struct TDB_Purse purse;
181 : struct TALER_CoinPublicInfo coin;
182 : struct TALER_PurseContractSignatureP purse_sig;
183 : struct TALER_CoinSpendSignatureP coin_sig;
184 1 : struct TALER_Amount amount = TDB_amount ("1");
185 : struct GNUNET_TIME_Timestamp purse_creation;
186 : struct GNUNET_TIME_Timestamp purse_expiration;
187 : struct GNUNET_TIME_Timestamp merge_timestamp;
188 : struct TALER_Amount target;
189 : struct TALER_Amount deposited;
190 : struct TALER_PrivateContractHashP h_contract_terms;
191 : struct DeletionContext ctx;
192 : bool balance_ok;
193 : bool too_late;
194 : bool conflict;
195 1 : bool decided = true;
196 1 : bool found = false;
197 1 : bool purse_deleted = false;
198 1 : bool purse_refunded = true;
199 : char *hex;
200 :
201 1 : TDB_purse (pg,
202 : 10,
203 : "5",
204 : ts (1700000000),
205 : &purse);
206 1 : TDB_coin (pg,
207 : &denom,
208 : 20,
209 : &coin,
210 : NULL);
211 1 : TDB_FILL (coin_sig,
212 : 20);
213 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
214 : TALER_EXCHANGEDB_do_purse_deposit (pg,
215 : &purse.purse_pub,
216 : &coin.coin_pub,
217 : &amount,
218 : &coin_sig,
219 : &amount,
220 : &balance_ok,
221 : &too_late,
222 : &conflict),
223 : TDB_coin_free (&coin));
224 1 : FAILIF_C (! balance_ok,
225 : TDB_coin_free (&coin));
226 :
227 1 : TDB_FILL (purse_sig,
228 : 10);
229 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
230 : TALER_EXCHANGEDB_do_purse_delete (pg,
231 : &purse.purse_pub,
232 : &purse_sig,
233 : &decided,
234 : &found),
235 : TDB_coin_free (&coin));
236 1 : FAILIF_C (! found,
237 : TDB_coin_free (&coin));
238 1 : FAILIF_C (decided,
239 : TDB_coin_free (&coin));
240 1 : FAILIF_C (1 != TDB_count (pg,
241 : "FROM purse_deletion"),
242 : TDB_coin_free (&coin));
243 : /* the coin got its money back */
244 1 : hex = TDB_hex (&coin.coin_pub,
245 : sizeof (coin.coin_pub));
246 1 : FAILIF_C (1 != TDB_count (pg,
247 : "FROM known_coins"
248 : " WHERE coin_pub=decode('%s','hex')"
249 : " AND remaining=ROW(5,0)::taler_amount",
250 : hex),
251 : GNUNET_free (hex); TDB_coin_free (&coin));
252 1 : GNUNET_free (hex);
253 1 : TDB_coin_free (&coin);
254 :
255 : /* the purse reports itself as deleted */
256 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
257 : TALER_EXCHANGEDB_get_purse (pg,
258 : &purse.purse_pub,
259 : &purse_creation,
260 : &purse_expiration,
261 : &target,
262 : &deposited,
263 : &h_contract_terms,
264 : &merge_timestamp,
265 : &purse_deleted,
266 : &purse_refunded));
267 1 : FAILIF (! purse_deleted);
268 :
269 : /* the deletion is reported with the signature it was made with */
270 1 : memset (&ctx,
271 : 0,
272 : sizeof (ctx));
273 1 : ctx.purse_pub = &purse.purse_pub;
274 1 : FAILIF (0 >=
275 : TALER_EXCHANGEDB_iterate_all_purse_deletions_above_serial_id (
276 : pg,
277 : 0,
278 : &deletion_cb,
279 : &ctx));
280 1 : FAILIF (1 != ctx.matched);
281 1 : FAILIF (0 != GNUNET_memcmp (&ctx.purse_sig,
282 : &purse_sig));
283 :
284 : /* deleting it again finds it already decided */
285 1 : decided = false;
286 1 : found = false;
287 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
288 : TALER_EXCHANGEDB_do_purse_delete (pg,
289 : &purse.purse_pub,
290 : &purse_sig,
291 : &decided,
292 : &found));
293 1 : FAILIF (! found);
294 1 : FAILIF (! decided);
295 1 : FAILIF (1 != TDB_count (pg,
296 : "FROM purse_deletion"));
297 1 : return 0;
298 : }
299 :
300 :
301 : /**
302 : * A purse that expired first can no longer be deleted.
303 : *
304 : * @param pg the database context
305 : * @return 0 on success
306 : */
307 : static int
308 1 : check_already_decided (struct TALER_EXCHANGEDB_PostgresContext *pg)
309 : {
310 : struct TDB_Purse purse;
311 : struct TALER_PurseContractSignatureP purse_sig;
312 : struct GNUNET_TIME_Absolute expiration;
313 1 : bool decided = false;
314 1 : bool found = false;
315 :
316 1 : TDB_purse (pg,
317 : 11,
318 : "5",
319 : ts (1600000000),
320 : &purse);
321 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
322 : TALER_EXCHANGEDB_do_expire_purse (pg,
323 : GNUNET_TIME_UNIT_ZERO_ABS,
324 : GNUNET_TIME_absolute_get (),
325 : &expiration));
326 1 : TDB_FILL (purse_sig,
327 : 11);
328 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
329 : TALER_EXCHANGEDB_do_purse_delete (pg,
330 : &purse.purse_pub,
331 : &purse_sig,
332 : &decided,
333 : &found));
334 1 : FAILIF (! found);
335 1 : FAILIF (! decided);
336 1 : FAILIF (1 != TDB_count (pg,
337 : "FROM purse_deletion"));
338 1 : return 0;
339 : }
340 :
341 :
342 : /**
343 : * The iterator's serial bound and abort return behave as documented.
344 : *
345 : * @param pg the database context
346 : * @return 0 on success
347 : */
348 : static int
349 1 : check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
350 : {
351 : struct DeletionContext ctx;
352 :
353 1 : memset (&ctx,
354 : 0,
355 : sizeof (ctx));
356 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
357 : TALER_EXCHANGEDB_iterate_all_purse_deletions_above_serial_id (
358 : pg,
359 : 1000,
360 : &deletion_cb,
361 : &ctx));
362 1 : FAILIF (0 != ctx.total);
363 1 : memset (&ctx,
364 : 0,
365 : sizeof (ctx));
366 1 : ctx.stop_after = 1;
367 1 : FAILIF (1 !=
368 : TALER_EXCHANGEDB_iterate_all_purse_deletions_above_serial_id (
369 : pg,
370 : 0,
371 : &deletion_cb,
372 : &ctx));
373 1 : FAILIF (1 != ctx.total);
374 1 : return 0;
375 : }
376 :
377 :
378 : /**
379 : * The checks to run, in order.
380 : */
381 : static const struct TDB_Test tests[] = {
382 : { "purse-deletion-unknown-purse",
383 : &check_unknown_purse },
384 : { "purse-deletion-delete",
385 : &check_delete },
386 : { "purse-deletion-already-decided",
387 : &check_already_decided },
388 : { "purse-deletion-iterate",
389 : &check_iterate },
390 : { NULL, NULL }
391 : };
392 :
393 :
394 : int
395 1 : main (int argc,
396 : char *const *argv)
397 : {
398 : int ret;
399 :
400 1 : ret = TDB_main (argc,
401 : argv,
402 : "test-purse-deletion",
403 : "Tests for the exchangedb `purse_deletion' table",
404 : tests);
405 1 : TDB_denom_free (&denom);
406 1 : return ret;
407 : }
408 :
409 :
410 : /* end of test_purse_deletion.c */
|