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_requests.c
18 : * @brief tests for the exchangedb functions whose primary table is
19 : * `purse_requests`
20 : * @author Christian Grothoff
21 : *
22 : * Covers #TALER_EXCHANGEDB_insert_purse_request(),
23 : * #TALER_EXCHANGEDB_get_purse_request(),
24 : * #TALER_EXCHANGEDB_get_purse_by_merge_pub(),
25 : * #TALER_EXCHANGEDB_get_purse() and
26 : * #TALER_EXCHANGEDB_iterate_purse_requests_above_serial_id().
27 : *
28 : * `purse_requests` has no foreign keys -- a purse is created before
29 : * anything is paid into it. The interesting case of the insert is the
30 : * conflict: re-creating a purse with the same key but different meta data
31 : * is reported through @e in_conflict rather than as an error.
32 : */
33 : #include "test_common.h"
34 : #include "exchange-database/get_purse.h"
35 : #include "exchange-database/get_purse_by_merge_pub.h"
36 : #include "exchange-database/get_purse_request.h"
37 : #include "exchange-database/insert_purse_request.h"
38 : #include "exchange-database/iterate_purse_requests_above_serial_id.h"
39 :
40 :
41 : /**
42 : * Build a timestamp from a number of seconds since the epoch.
43 : *
44 : * @param secs seconds since the epoch
45 : * @return the timestamp
46 : */
47 : static struct GNUNET_TIME_Timestamp
48 3 : ts (uint64_t secs)
49 : {
50 3 : struct GNUNET_TIME_Absolute abs = {
51 3 : .abs_value_us = secs * 1000LLU * 1000LLU
52 : };
53 :
54 3 : return GNUNET_TIME_absolute_to_timestamp (abs);
55 : }
56 :
57 :
58 : /**
59 : * Closure for #purse_cb().
60 : */
61 : struct PurseContext
62 : {
63 : /**
64 : * How many rows did the callback see?
65 : */
66 : unsigned int total;
67 :
68 : /**
69 : * Stop after this many rows; 0 for no limit.
70 : */
71 : unsigned int stop_after;
72 :
73 : /**
74 : * Purse we are looking for, NULL to match nothing.
75 : */
76 : const struct TALER_PurseContractPublicKeyP *purse_pub;
77 :
78 : /**
79 : * How many times did we see it?
80 : */
81 : unsigned int matched;
82 :
83 : /**
84 : * Target amount reported for it.
85 : */
86 : struct TALER_Amount amount;
87 :
88 : /**
89 : * Age limit reported for it.
90 : */
91 : uint32_t age_limit;
92 : };
93 :
94 :
95 : /**
96 : * Callback for #TALER_EXCHANGEDB_iterate_purse_requests_above_serial_id().
97 : *
98 : * @param cls a `struct PurseContext *`
99 : * @param rowid row of the purse
100 : * @param purse_pub the purse
101 : * @param merge_pub its merge capability key
102 : * @param purse_creation when it was created
103 : * @param purse_expiration when it expires
104 : * @param h_contract_terms contract it is for
105 : * @param age_limit age limit on deposits into it
106 : * @param target_amount how much it is meant to hold
107 : * @param purse_sig signature over its meta data
108 : * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
109 : */
110 : static enum GNUNET_GenericReturnValue
111 3 : purse_cb (void *cls,
112 : uint64_t rowid,
113 : const struct TALER_PurseContractPublicKeyP *purse_pub,
114 : const struct TALER_PurseMergePublicKeyP *merge_pub,
115 : struct GNUNET_TIME_Timestamp purse_creation,
116 : struct GNUNET_TIME_Timestamp purse_expiration,
117 : const struct TALER_PrivateContractHashP *h_contract_terms,
118 : uint32_t age_limit,
119 : const struct TALER_Amount *target_amount,
120 : const struct TALER_PurseContractSignatureP *purse_sig)
121 : {
122 3 : struct PurseContext *ctx = cls;
123 :
124 : (void) rowid;
125 : (void) merge_pub;
126 : (void) purse_creation;
127 : (void) purse_expiration;
128 : (void) h_contract_terms;
129 : (void) purse_sig;
130 3 : ctx->total++;
131 3 : if ( (NULL != ctx->purse_pub) &&
132 2 : (0 == GNUNET_memcmp (purse_pub,
133 : ctx->purse_pub)) )
134 : {
135 1 : ctx->matched++;
136 1 : ctx->amount = *target_amount;
137 1 : ctx->age_limit = age_limit;
138 : }
139 3 : if ( (0 != ctx->stop_after) &&
140 1 : (ctx->total >= ctx->stop_after) )
141 1 : return GNUNET_SYSERR;
142 2 : return GNUNET_OK;
143 : }
144 :
145 :
146 : /**
147 : * Nothing is known while the table is empty.
148 : *
149 : * @param pg the database context
150 : * @return 0 on success
151 : */
152 : static int
153 1 : check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
154 : {
155 : struct TALER_PurseContractPublicKeyP purse_pub;
156 : struct TALER_PurseMergePublicKeyP merge_pub;
157 : struct GNUNET_TIME_Timestamp purse_expiration;
158 : struct GNUNET_TIME_Timestamp purse_creation;
159 : struct GNUNET_TIME_Timestamp merge_timestamp;
160 : struct TALER_PrivateContractHashP h_contract_terms;
161 : struct TALER_Amount target_amount;
162 : struct TALER_Amount balance;
163 : struct TALER_PurseContractSignatureP purse_sig;
164 1 : struct PurseContext ctx = { 0 };
165 : uint32_t age_limit;
166 : bool purse_deleted;
167 : bool purse_refunded;
168 :
169 1 : TDB_FILL (purse_pub,
170 : 1);
171 1 : TDB_FILL (merge_pub,
172 : 1);
173 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
174 : TALER_EXCHANGEDB_get_purse_request (pg,
175 : &purse_pub,
176 : &merge_pub,
177 : &purse_expiration,
178 : &h_contract_terms,
179 : &age_limit,
180 : &target_amount,
181 : &balance,
182 : &purse_sig));
183 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
184 : TALER_EXCHANGEDB_get_purse_by_merge_pub (pg,
185 : &merge_pub,
186 : &purse_pub,
187 : &purse_expiration,
188 : &h_contract_terms,
189 : &age_limit,
190 : &target_amount,
191 : &balance,
192 : &purse_sig));
193 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
194 : TALER_EXCHANGEDB_get_purse (pg,
195 : &purse_pub,
196 : &purse_creation,
197 : &purse_expiration,
198 : &target_amount,
199 : &balance,
200 : &h_contract_terms,
201 : &merge_timestamp,
202 : &purse_deleted,
203 : &purse_refunded));
204 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
205 : TALER_EXCHANGEDB_iterate_purse_requests_above_serial_id (
206 : pg,
207 : 0,
208 : &purse_cb,
209 : &ctx));
210 1 : FAILIF (0 != ctx.total);
211 1 : return 0;
212 : }
213 :
214 :
215 : /**
216 : * A created purse is found by its own key, by its merge key and by the
217 : * combined status lookup.
218 : *
219 : * @param pg the database context
220 : * @return 0 on success
221 : */
222 : static int
223 1 : check_create_and_lookup (struct TALER_EXCHANGEDB_PostgresContext *pg)
224 : {
225 : struct TDB_Purse purse;
226 : struct TALER_PurseContractPublicKeyP got_pub;
227 : struct TALER_PurseMergePublicKeyP got_merge;
228 : struct GNUNET_TIME_Timestamp purse_expiration;
229 : struct GNUNET_TIME_Timestamp purse_creation;
230 : struct GNUNET_TIME_Timestamp merge_timestamp;
231 : struct TALER_PrivateContractHashP h_contract_terms;
232 : struct TALER_Amount target_amount;
233 : struct TALER_Amount balance;
234 1 : struct TALER_Amount zero = TDB_amount ("0");
235 : struct TALER_PurseContractSignatureP purse_sig;
236 : uint32_t age_limit;
237 1 : bool purse_deleted = true;
238 1 : bool purse_refunded = true;
239 :
240 1 : TDB_purse (pg,
241 : 10,
242 : "5",
243 : ts (1700000000),
244 : &purse);
245 1 : FAILIF (1 != TDB_count (pg,
246 : "FROM purse_requests"));
247 :
248 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
249 : TALER_EXCHANGEDB_get_purse_request (pg,
250 : &purse.purse_pub,
251 : &got_merge,
252 : &purse_expiration,
253 : &h_contract_terms,
254 : &age_limit,
255 : &target_amount,
256 : &balance,
257 : &purse_sig));
258 1 : FAILIF (0 != GNUNET_memcmp (&got_merge,
259 : &purse.merge_pub));
260 1 : FAILIF (0 != GNUNET_memcmp (&h_contract_terms,
261 : &purse.h_contract_terms));
262 1 : FAILIF (0 != GNUNET_memcmp (&purse_sig,
263 : &purse.purse_sig));
264 1 : FAILIF (GNUNET_TIME_timestamp_cmp (purse_expiration,
265 : !=,
266 : purse.purse_expiration));
267 1 : FAILIF (0 != TALER_amount_cmp (&target_amount,
268 : &purse.amount));
269 : /* nothing has been paid in yet */
270 1 : FAILIF (0 != TALER_amount_cmp (&balance,
271 : &zero));
272 1 : FAILIF (0 != age_limit);
273 :
274 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
275 : TALER_EXCHANGEDB_get_purse_by_merge_pub (pg,
276 : &purse.merge_pub,
277 : &got_pub,
278 : &purse_expiration,
279 : &h_contract_terms,
280 : &age_limit,
281 : &target_amount,
282 : &balance,
283 : &purse_sig));
284 1 : FAILIF (0 != GNUNET_memcmp (&got_pub,
285 : &purse.purse_pub));
286 :
287 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
288 : TALER_EXCHANGEDB_get_purse (pg,
289 : &purse.purse_pub,
290 : &purse_creation,
291 : &purse_expiration,
292 : &target_amount,
293 : &balance,
294 : &h_contract_terms,
295 : &merge_timestamp,
296 : &purse_deleted,
297 : &purse_refunded));
298 1 : FAILIF (purse_deleted);
299 1 : FAILIF (purse_refunded);
300 1 : FAILIF (0 != TALER_amount_cmp (&balance,
301 : &zero));
302 : /* the purse has not been merged */
303 1 : FAILIF (! GNUNET_TIME_absolute_is_never (merge_timestamp.abs_time));
304 :
305 : /* a merge key nobody used finds nothing */
306 : {
307 : struct TALER_PurseMergePublicKeyP other;
308 :
309 1 : TDB_FILL (other,
310 : 99);
311 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
312 : TALER_EXCHANGEDB_get_purse_by_merge_pub (pg,
313 : &other,
314 : &got_pub,
315 : &purse_expiration,
316 : &h_contract_terms,
317 : &age_limit,
318 : &target_amount,
319 : &balance,
320 : &purse_sig));
321 : }
322 1 : return 0;
323 : }
324 :
325 :
326 : /**
327 : * Re-creating a purse with the same key is idempotent if the meta data
328 : * matches and a conflict if it does not.
329 : *
330 : * @param pg the database context
331 : * @return 0 on success
332 : */
333 : static int
334 1 : check_conflict (struct TALER_EXCHANGEDB_PostgresContext *pg)
335 : {
336 : struct TDB_Purse purse;
337 1 : struct TALER_Amount purse_fee = TDB_amount ("0");
338 1 : struct TALER_Amount other_amount = TDB_amount ("9");
339 1 : bool in_conflict = false;
340 :
341 : /* TDB_purse() with the same seed replays the identical request */
342 1 : TDB_purse (pg,
343 : 10,
344 : "5",
345 : ts (1700000000),
346 : &purse);
347 1 : FAILIF (1 != TDB_count (pg,
348 : "FROM purse_requests"));
349 : /* An identical replay is reported as "nothing to do". Note that the
350 : function leaves @e in_conflict untouched on that path, so only the
351 : status says so. */
352 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
353 : TALER_EXCHANGEDB_insert_purse_request (
354 : pg,
355 : &purse.purse_pub,
356 : &purse.merge_pub,
357 : purse.purse_expiration,
358 : &purse.h_contract_terms,
359 : 0,
360 : TALER_WAMF_MODE_CREATE_FROM_PURSE_QUOTA,
361 : &purse_fee,
362 : &purse.amount,
363 : &purse.purse_sig,
364 : &in_conflict));
365 1 : FAILIF (in_conflict);
366 :
367 : /* the same purse key with a different target amount is a conflict */
368 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
369 : TALER_EXCHANGEDB_insert_purse_request (
370 : pg,
371 : &purse.purse_pub,
372 : &purse.merge_pub,
373 : purse.purse_expiration,
374 : &purse.h_contract_terms,
375 : 0,
376 : TALER_WAMF_MODE_CREATE_FROM_PURSE_QUOTA,
377 : &purse_fee,
378 : &other_amount,
379 : &purse.purse_sig,
380 : &in_conflict));
381 1 : FAILIF (! in_conflict);
382 1 : FAILIF (1 != TDB_count (pg,
383 : "FROM purse_requests"));
384 1 : return 0;
385 : }
386 :
387 :
388 : /**
389 : * The iterator walks the table by serial and honours an aborting
390 : * callback.
391 : *
392 : * @param pg the database context
393 : * @return 0 on success
394 : */
395 : static int
396 1 : check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
397 : {
398 : struct TDB_Purse p2;
399 : struct PurseContext ctx;
400 :
401 1 : TDB_purse (pg,
402 : 11,
403 : "3",
404 : ts (1700003600),
405 : &p2);
406 1 : FAILIF (2 != TDB_count (pg,
407 : "FROM purse_requests"));
408 1 : memset (&ctx,
409 : 0,
410 : sizeof (ctx));
411 1 : ctx.purse_pub = &p2.purse_pub;
412 1 : FAILIF (2 !=
413 : TALER_EXCHANGEDB_iterate_purse_requests_above_serial_id (
414 : pg,
415 : 0,
416 : &purse_cb,
417 : &ctx));
418 1 : FAILIF (1 != ctx.matched);
419 1 : FAILIF (0 != TALER_amount_cmp (&ctx.amount,
420 : &p2.amount));
421 1 : FAILIF (0 != ctx.age_limit);
422 :
423 1 : memset (&ctx,
424 : 0,
425 : sizeof (ctx));
426 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
427 : TALER_EXCHANGEDB_iterate_purse_requests_above_serial_id (
428 : pg,
429 : 1000,
430 : &purse_cb,
431 : &ctx));
432 1 : FAILIF (0 != ctx.total);
433 :
434 1 : memset (&ctx,
435 : 0,
436 : sizeof (ctx));
437 1 : ctx.stop_after = 1;
438 1 : FAILIF (2 !=
439 : TALER_EXCHANGEDB_iterate_purse_requests_above_serial_id (
440 : pg,
441 : 0,
442 : &purse_cb,
443 : &ctx));
444 1 : FAILIF (1 != ctx.total);
445 1 : return 0;
446 : }
447 :
448 :
449 : /**
450 : * The checks to run, in order.
451 : */
452 : static const struct TDB_Test tests[] = {
453 : { "purse-requests-empty",
454 : &check_empty },
455 : { "purse-requests-create-and-lookup",
456 : &check_create_and_lookup },
457 : { "purse-requests-conflict",
458 : &check_conflict },
459 : { "purse-requests-iterate",
460 : &check_iterate },
461 : { NULL, NULL }
462 : };
463 :
464 :
465 : int
466 1 : main (int argc,
467 : char *const *argv)
468 : {
469 1 : return TDB_main (argc,
470 : argv,
471 : "test-purse-requests",
472 : "Tests for the exchangedb `purse_requests' table",
473 : tests);
474 : }
475 :
476 :
477 : /* end of test_purse_requests.c */
|