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_close_requests.c
18 : * @brief tests for the exchangedb functions whose primary table is
19 : * `close_requests`
20 : * @author Christian Grothoff
21 : *
22 : * Covers #TALER_EXCHANGEDB_insert_close_request(),
23 : * #TALER_EXCHANGEDB_get_reserve_close_request_info() and
24 : * #TALER_EXCHANGEDB_iterate_unfinished_close_requests().
25 : *
26 : * The table references `reserves`, which TDB_reserve() creates. The
27 : * iterator is destructive -- it marks everything it returns as done -- so
28 : * the checks that read the table run before it.
29 : */
30 : #include "test_common.h"
31 : #include "exchange-database/insert_close_request.h"
32 : #include "exchange-database/get_reserve_close_request_info.h"
33 : #include "exchange-database/iterate_unfinished_close_requests.h"
34 :
35 :
36 : /**
37 : * Account the checks ask to be paid out to.
38 : */
39 : #define PAYTO "payto://x-taler-bank/localhost/cr?receiver-name=CR"
40 :
41 :
42 : /**
43 : * Build a timestamp from a number of seconds since the epoch.
44 : *
45 : * @param secs seconds since the epoch
46 : * @return the timestamp
47 : */
48 : static struct GNUNET_TIME_Timestamp
49 5 : ts (uint64_t secs)
50 : {
51 5 : struct GNUNET_TIME_Absolute abs = {
52 5 : .abs_value_us = secs * 1000LLU * 1000LLU
53 : };
54 :
55 5 : return GNUNET_TIME_absolute_to_timestamp (abs);
56 : }
57 :
58 :
59 : /**
60 : * Closure for #expired_cb().
61 : */
62 : struct ExpiredContext
63 : {
64 : /**
65 : * How many requests did the callback see?
66 : */
67 : unsigned int total;
68 :
69 : /**
70 : * Reserve of the last request seen.
71 : */
72 : struct TALER_ReservePublicKeyP reserve_pub;
73 :
74 : /**
75 : * Balance of the last request seen.
76 : */
77 : struct TALER_Amount left;
78 :
79 : /**
80 : * Row of the last request seen.
81 : */
82 : uint64_t close_request_row;
83 :
84 : /**
85 : * Account of the last request seen, owned by this struct.
86 : */
87 : char *account;
88 : };
89 :
90 :
91 : /**
92 : * Callback for #TALER_EXCHANGEDB_iterate_unfinished_close_requests().
93 : *
94 : * @param cls a `struct ExpiredContext *`
95 : * @param reserve_pub the reserve to close
96 : * @param left balance to pay out
97 : * @param account_details where to pay it
98 : * @param expiration_date when the request was made
99 : * @param close_request_row row of the request
100 : * @return #GNUNET_OK
101 : */
102 : static enum GNUNET_GenericReturnValue
103 3 : expired_cb (void *cls,
104 : const struct TALER_ReservePublicKeyP *reserve_pub,
105 : const struct TALER_Amount *left,
106 : const struct TALER_FullPayto account_details,
107 : struct GNUNET_TIME_Timestamp expiration_date,
108 : uint64_t close_request_row)
109 : {
110 3 : struct ExpiredContext *ctx = cls;
111 :
112 : (void) expiration_date;
113 3 : ctx->total++;
114 3 : ctx->reserve_pub = *reserve_pub;
115 3 : ctx->left = *left;
116 3 : ctx->close_request_row = close_request_row;
117 3 : GNUNET_free (ctx->account);
118 6 : ctx->account = (NULL == account_details.full_payto)
119 : ? NULL
120 3 : : GNUNET_strdup (account_details.full_payto);
121 3 : return GNUNET_OK;
122 : }
123 :
124 :
125 : /**
126 : * Look up the row of the close request of a reserve.
127 : *
128 : * @param pg the database context
129 : * @param reserve_pub reserve to look up
130 : * @return the row
131 : */
132 : static uint64_t
133 1 : request_row (struct TALER_EXCHANGEDB_PostgresContext *pg,
134 : const struct TALER_ReservePublicKeyP *reserve_pub)
135 : {
136 1 : struct GNUNET_PQ_QueryParam params[] = {
137 1 : GNUNET_PQ_query_param_auto_from_type (reserve_pub),
138 : GNUNET_PQ_query_param_end
139 : };
140 : uint64_t row;
141 1 : struct GNUNET_PQ_ResultSpec rs[] = {
142 1 : GNUNET_PQ_result_spec_uint64 ("close_request_serial_id",
143 : &row),
144 : GNUNET_PQ_result_spec_end
145 : };
146 :
147 1 : GNUNET_assert (GNUNET_OK ==
148 : GNUNET_PQ_prepare_anon (pg->conn,
149 : "SELECT close_request_serial_id"
150 : " FROM close_requests"
151 : " WHERE reserve_pub=$1;"));
152 1 : GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
153 : GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
154 : "",
155 : params,
156 : rs));
157 1 : return row;
158 : }
159 :
160 :
161 : /**
162 : * Ask for a reserve to be closed.
163 : *
164 : * @param pg the database context
165 : * @param reserve_pub reserve to close
166 : * @param seed seed for the reserve signature
167 : * @param when when the request was made, in seconds since the epoch
168 : * @param balance balance to pay out, e.g. "10"
169 : * @param fee closing fee, e.g. "0.5"
170 : * @return transaction status
171 : */
172 : static enum GNUNET_DB_QueryStatus
173 4 : add_request (struct TALER_EXCHANGEDB_PostgresContext *pg,
174 : const struct TALER_ReservePublicKeyP *reserve_pub,
175 : uint32_t seed,
176 : uint64_t when,
177 : const char *balance,
178 : const char *fee)
179 : {
180 4 : struct TALER_FullPayto payto = {
181 : .full_payto = (char *) PAYTO
182 : };
183 : struct TALER_ReserveSignatureP reserve_sig;
184 4 : struct TALER_Amount b = TDB_amount (balance);
185 4 : struct TALER_Amount f = TDB_amount (fee);
186 :
187 4 : TDB_fill (&reserve_sig,
188 : sizeof (reserve_sig),
189 : seed);
190 4 : return TALER_EXCHANGEDB_insert_close_request (pg,
191 : reserve_pub,
192 : payto,
193 : &reserve_sig,
194 : ts (when),
195 : &b,
196 : &f);
197 : }
198 :
199 :
200 : /**
201 : * Nothing is known while the table is empty.
202 : *
203 : * @param pg the database context
204 : * @return 0 on success
205 : */
206 : static int
207 1 : check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
208 : {
209 : struct TALER_ReservePublicKeyP reserve_pub;
210 : struct TALER_ReserveSignatureP reserve_sig;
211 : struct GNUNET_TIME_Timestamp request_timestamp;
212 : struct TALER_Amount close_balance;
213 : struct TALER_Amount close_fee;
214 1 : struct TALER_FullPayto payto = { NULL };
215 1 : struct ExpiredContext ctx = { 0 };
216 :
217 1 : TDB_FILL (reserve_pub,
218 : 1);
219 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
220 : TALER_EXCHANGEDB_get_reserve_close_request_info (pg,
221 : &reserve_pub,
222 : 1,
223 : &reserve_sig,
224 : &request_timestamp,
225 : &close_balance,
226 : &close_fee,
227 : &payto));
228 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
229 : TALER_EXCHANGEDB_iterate_unfinished_close_requests (pg,
230 : &expired_cb,
231 : &ctx));
232 1 : FAILIF (0 != ctx.total);
233 1 : return 0;
234 : }
235 :
236 :
237 : /**
238 : * A close request is stored and found by (reserve, row).
239 : *
240 : * @param pg the database context
241 : * @return 0 on success
242 : */
243 : static int
244 1 : check_insert_and_lookup (struct TALER_EXCHANGEDB_PostgresContext *pg)
245 : {
246 : struct TALER_ReservePublicKeyP reserve_pub;
247 : struct TALER_ReserveSignatureP reserve_sig;
248 : struct TALER_ReserveSignatureP expect_sig;
249 : struct GNUNET_TIME_Timestamp request_timestamp;
250 : struct TALER_Amount close_balance;
251 : struct TALER_Amount close_fee;
252 1 : struct TALER_Amount expect_balance = TDB_amount ("10");
253 1 : struct TALER_Amount expect_fee = TDB_amount ("0.5");
254 1 : struct TALER_FullPayto payto = { NULL };
255 : uint64_t row;
256 :
257 1 : TDB_reserve (pg,
258 : 10,
259 : "10",
260 : &reserve_pub);
261 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
262 : add_request (pg,
263 : &reserve_pub,
264 : 10,
265 : 1600000000,
266 : "10",
267 : "0.5"));
268 1 : row = request_row (pg,
269 : &reserve_pub);
270 1 : TDB_FILL (expect_sig,
271 : 10);
272 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
273 : TALER_EXCHANGEDB_get_reserve_close_request_info (pg,
274 : &reserve_pub,
275 : row,
276 : &reserve_sig,
277 : &request_timestamp,
278 : &close_balance,
279 : &close_fee,
280 : &payto));
281 1 : FAILIF (0 != GNUNET_memcmp (&reserve_sig,
282 : &expect_sig));
283 1 : FAILIF (GNUNET_TIME_timestamp_cmp (request_timestamp,
284 : !=,
285 : ts (1600000000)));
286 1 : FAILIF (0 != TALER_amount_cmp (&close_balance,
287 : &expect_balance));
288 1 : FAILIF (0 != TALER_amount_cmp (&close_fee,
289 : &expect_fee));
290 1 : FAILIF (NULL == payto.full_payto);
291 1 : FAILIF_C (0 != strcmp (payto.full_payto,
292 : PAYTO),
293 : GNUNET_free (payto.full_payto));
294 1 : GNUNET_free (payto.full_payto);
295 :
296 : /* the row has to belong to the reserve that is asked about */
297 1 : payto.full_payto = NULL;
298 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
299 : TALER_EXCHANGEDB_get_reserve_close_request_info (pg,
300 : &reserve_pub,
301 : row + 1000,
302 : &reserve_sig,
303 : &request_timestamp,
304 : &close_balance,
305 : &close_fee,
306 : &payto));
307 : {
308 : struct TALER_ReservePublicKeyP other;
309 :
310 1 : TDB_FILL (other,
311 : 99);
312 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
313 : TALER_EXCHANGEDB_get_reserve_close_request_info (pg,
314 : &other,
315 : row,
316 : &reserve_sig,
317 : &request_timestamp,
318 : &close_balance,
319 : &close_fee,
320 : &payto));
321 : }
322 :
323 : /* a second request at the same timestamp is absorbed */
324 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
325 : add_request (pg,
326 : &reserve_pub,
327 : 11,
328 : 1600000000,
329 : "10",
330 : "0.5"));
331 1 : FAILIF (1 != TDB_count (pg,
332 : "FROM close_requests"));
333 : /* ...but one at another timestamp is a second request */
334 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
335 : add_request (pg,
336 : &reserve_pub,
337 : 11,
338 : 1600003600,
339 : "10",
340 : "0.5"));
341 1 : FAILIF (2 != TDB_count (pg,
342 : "FROM close_requests"));
343 1 : return 0;
344 : }
345 :
346 :
347 : /**
348 : * The iterator hands over every unfinished request exactly once, marking
349 : * it done as it goes.
350 : *
351 : * @param pg the database context
352 : * @return 0 on success
353 : */
354 : static int
355 1 : check_iterate_unfinished (struct TALER_EXCHANGEDB_PostgresContext *pg)
356 : {
357 : struct ExpiredContext ctx;
358 :
359 1 : FAILIF (2 != TDB_count (pg,
360 : "FROM close_requests WHERE NOT done"));
361 1 : memset (&ctx,
362 : 0,
363 : sizeof (ctx));
364 1 : FAILIF (2 !=
365 : TALER_EXCHANGEDB_iterate_unfinished_close_requests (pg,
366 : &expired_cb,
367 : &ctx));
368 1 : FAILIF_C (2 != ctx.total,
369 : GNUNET_free (ctx.account));
370 1 : FAILIF_C (NULL == ctx.account,
371 : GNUNET_free (ctx.account));
372 1 : FAILIF_C (0 != strcmp (ctx.account,
373 : PAYTO),
374 : GNUNET_free (ctx.account));
375 1 : FAILIF_C (0 == ctx.close_request_row,
376 : GNUNET_free (ctx.account));
377 1 : GNUNET_free (ctx.account);
378 1 : FAILIF (0 != TDB_count (pg,
379 : "FROM close_requests WHERE NOT done"));
380 :
381 : /* everything is done now, so a second sweep finds nothing */
382 1 : memset (&ctx,
383 : 0,
384 : sizeof (ctx));
385 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
386 : TALER_EXCHANGEDB_iterate_unfinished_close_requests (pg,
387 : &expired_cb,
388 : &ctx));
389 1 : FAILIF (0 != ctx.total);
390 :
391 : /* a fresh request is picked up again */
392 : {
393 : struct TALER_ReservePublicKeyP reserve_pub;
394 :
395 1 : TDB_reserve (pg,
396 : 12,
397 : "3",
398 : &reserve_pub);
399 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
400 : add_request (pg,
401 : &reserve_pub,
402 : 12,
403 : 1600007200,
404 : "3",
405 : "0.5"));
406 1 : memset (&ctx,
407 : 0,
408 : sizeof (ctx));
409 1 : FAILIF (1 !=
410 : TALER_EXCHANGEDB_iterate_unfinished_close_requests (pg,
411 : &expired_cb,
412 : &ctx));
413 1 : FAILIF_C (0 != GNUNET_memcmp (&ctx.reserve_pub,
414 : &reserve_pub),
415 : GNUNET_free (ctx.account));
416 1 : GNUNET_free (ctx.account);
417 : }
418 1 : return 0;
419 : }
420 :
421 :
422 : /**
423 : * The checks to run, in order.
424 : */
425 : static const struct TDB_Test tests[] = {
426 : { "close-requests-empty",
427 : &check_empty },
428 : { "close-requests-insert-and-lookup",
429 : &check_insert_and_lookup },
430 : { "close-requests-iterate-unfinished",
431 : &check_iterate_unfinished },
432 : { NULL, NULL }
433 : };
434 :
435 :
436 : int
437 1 : main (int argc,
438 : char *const *argv)
439 : {
440 1 : return TDB_main (argc,
441 : argv,
442 : "test-close-requests",
443 : "Tests for the exchangedb `close_requests' table",
444 : tests);
445 : }
446 :
447 :
448 : /* end of test_close_requests.c */
|